summaryrefslogtreecommitdiff
path: root/localwebsite
diff options
context:
space:
mode:
Diffstat (limited to 'localwebsite')
-rw-r--r--localwebsite/classes/E3372.php292
-rw-r--r--localwebsite/classes/GPIORelaydClient.php18
-rw-r--r--localwebsite/classes/InverterdClient.php69
-rw-r--r--localwebsite/classes/MyOpenWrtUtils.php133
-rw-r--r--localwebsite/classes/MySimpleSocketClient.php90
-rw-r--r--localwebsite/classes/Si7021dClient.php31
-rw-r--r--localwebsite/composer.json14
-rw-r--r--localwebsite/composer.lock205
-rw-r--r--localwebsite/config.php58
-rw-r--r--localwebsite/engine/debug.php355
-rw-r--r--localwebsite/engine/request_handler.php142
-rw-r--r--localwebsite/engine/router.php199
-rw-r--r--localwebsite/engine/tpl.php520
-rw-r--r--localwebsite/functions.php255
-rw-r--r--localwebsite/handlers/FakeRequestHandler.php20
-rw-r--r--localwebsite/handlers/InverterHandler.php88
-rw-r--r--localwebsite/handlers/MiscHandler.php52
-rw-r--r--localwebsite/handlers/ModemHandler.php192
-rw-r--r--localwebsite/handlers/RequestHandler.php41
-rw-r--r--localwebsite/htdocs/assets/app.css153
-rw-r--r--localwebsite/htdocs/assets/app.js44
-rw-r--r--localwebsite/htdocs/assets/bootstrap.min.css7
-rw-r--r--localwebsite/htdocs/assets/bootstrap.min.js7
-rw-r--r--localwebsite/htdocs/assets/inverter.js15
-rw-r--r--localwebsite/htdocs/assets/modem.js29
-rw-r--r--localwebsite/htdocs/assets/polyfills.js560
-rw-r--r--localwebsite/htdocs/favicon.icobin0 -> 1150 bytes
-rw-r--r--localwebsite/htdocs/index.php44
-rw-r--r--localwebsite/init.php71
-rw-r--r--localwebsite/templates-web/404.twig1
-rw-r--r--localwebsite/templates-web/footer.twig8
-rw-r--r--localwebsite/templates-web/header.twig10
-rw-r--r--localwebsite/templates-web/index.twig21
-rw-r--r--localwebsite/templates-web/inverter_page.twig15
-rw-r--r--localwebsite/templates-web/modem_data.twig12
-rw-r--r--localwebsite/templates-web/modem_status_page.twig19
-rw-r--r--localwebsite/templates-web/pump.twig18
-rw-r--r--localwebsite/templates-web/routing_dhcp_page.twig11
-rw-r--r--localwebsite/templates-web/routing_header.twig24
-rw-r--r--localwebsite/templates-web/routing_ipsets_page.twig29
-rw-r--r--localwebsite/templates-web/routing_page.twig17
-rw-r--r--localwebsite/templates-web/sensors.twig12
-rw-r--r--localwebsite/templates-web/signal_level.twig5
-rw-r--r--localwebsite/templates-web/sms_page.twig27
-rw-r--r--localwebsite/templates-web/spinner.twig14
45 files changed, 3947 insertions, 0 deletions
diff --git a/localwebsite/classes/E3372.php b/localwebsite/classes/E3372.php
new file mode 100644
index 0000000..538d387
--- /dev/null
+++ b/localwebsite/classes/E3372.php
@@ -0,0 +1,292 @@
+<?php
+
+class E3372
+{
+
+ const WIFI_CONNECTING = '900';
+ const WIFI_CONNECTED = '901';
+ const WIFI_DISCONNECTED = '902';
+ const WIFI_DISCONNECTING = '903';
+
+ const CRADLE_CONNECTING = '900';
+ const CRADLE_CONNECTED = '901';
+ const CRADLE_DISCONNECTED = '902';
+ const CRADLE_DISCONNECTING = '903';
+ const CRADLE_CONNECTFAILED = '904';
+ const CRADLE_CONNECTSTATUSNULL = '905';
+ const CRANDLE_CONNECTSTATUSERRO = '906';
+
+ const MACRO_EVDO_LEVEL_ZERO = '0';
+ const MACRO_EVDO_LEVEL_ONE = '1';
+ const MACRO_EVDO_LEVEL_TWO = '2';
+ const MACRO_EVDO_LEVEL_THREE = '3';
+ const MACRO_EVDO_LEVEL_FOUR = '4';
+ const MACRO_EVDO_LEVEL_FIVE = '5';
+
+ // CurrentNetworkType
+ const MACRO_NET_WORK_TYPE_NOSERVICE = 0;
+ const MACRO_NET_WORK_TYPE_GSM = 1;
+ const MACRO_NET_WORK_TYPE_GPRS = 2;
+ const MACRO_NET_WORK_TYPE_EDGE = 3;
+ const MACRO_NET_WORK_TYPE_WCDMA = 4;
+ const MACRO_NET_WORK_TYPE_HSDPA = 5;
+ const MACRO_NET_WORK_TYPE_HSUPA = 6;
+ const MACRO_NET_WORK_TYPE_HSPA = 7;
+ const MACRO_NET_WORK_TYPE_TDSCDMA = 8;
+ const MACRO_NET_WORK_TYPE_HSPA_PLUS = 9;
+ const MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10;
+ const MACRO_NET_WORK_TYPE_EVDO_REV_A = 11;
+ const MACRO_NET_WORK_TYPE_EVDO_REV_B = 12;
+ const MACRO_NET_WORK_TYPE_1xRTT = 13;
+ const MACRO_NET_WORK_TYPE_UMB = 14;
+ const MACRO_NET_WORK_TYPE_1xEVDV = 15;
+ const MACRO_NET_WORK_TYPE_3xRTT = 16;
+ const MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17;
+ const MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18;
+ const MACRO_NET_WORK_TYPE_LTE = 19;
+ const MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0;
+ const MACRO_NET_WORK_TYPE_EX_GSM = 1;
+ const MACRO_NET_WORK_TYPE_EX_GPRS = 2;
+ const MACRO_NET_WORK_TYPE_EX_EDGE = 3;
+ const MACRO_NET_WORK_TYPE_EX_IS95A = 21;
+ const MACRO_NET_WORK_TYPE_EX_IS95B = 22;
+ const MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23;
+ const MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24;
+ const MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25;
+ const MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26;
+ const MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27;
+ const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28;
+ const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29;
+ const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30;
+ const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31;
+ const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32;
+ const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33;
+ const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34;
+ const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35;
+ const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36;
+ const MACRO_NET_WORK_TYPE_EX_WCDMA = 41;
+ const MACRO_NET_WORK_TYPE_EX_HSDPA = 42;
+ const MACRO_NET_WORK_TYPE_EX_HSUPA = 43;
+ const MACRO_NET_WORK_TYPE_EX_HSPA = 44;
+ const MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45;
+ const MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46;
+ const MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61;
+ const MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62;
+ const MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63;
+ const MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64;
+ const MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65;
+ const MACRO_NET_WORK_TYPE_EX_802_16E = 81;
+ const MACRO_NET_WORK_TYPE_EX_LTE = 101;
+
+
+ const ERROR_SYSTEM_NO_SUPPORT = 100002;
+ const ERROR_SYSTEM_NO_RIGHTS = 100003;
+ const ERROR_SYSTEM_BUSY = 100004;
+ const ERROR_LOGIN_USERNAME_WRONG = 108001;
+ const ERROR_LOGIN_PASSWORD_WRONG = 108002;
+ const ERROR_LOGIN_ALREADY_LOGIN = 108003;
+ const ERROR_LOGIN_USERNAME_PWD_WRONG = 108006;
+ const ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007;
+ const ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009;
+ const ERROR_VOICE_BUSY = 120001;
+ const ERROR_WRONG_TOKEN = 125001;
+ const ERROR_WRONG_SESSION = 125002;
+ const ERROR_WRONG_SESSION_TOKEN = 125003;
+
+
+ private $host;
+ private $headers = [];
+ private $authorized = false;
+ private $useLegacyTokenAuth = false;
+
+ public function __construct(string $host, bool $legacy_token_auth = false) {
+ $this->host = $host;
+ $this->useLegacyTokenAuth = $legacy_token_auth;
+ }
+
+ public function __call(string $name, array $arguments) {
+ if (startsWith($name, 'get'))
+ $this->auth();
+
+ return call_user_func_array([$this, $name], $arguments);
+ }
+
+ public function auth() {
+ if ($this->authorized)
+ return;
+
+ if (!$this->useLegacyTokenAuth) {
+ $data = $this->request('webserver/SesTokInfo');
+ $this->headers = [
+ 'Cookie: '.$data['SesInfo'],
+ '__RequestVerificationToken: '.$data['TokInfo'],
+ 'Content-Type: text/xml'
+ ];
+ } else {
+ $data = $this->request('webserver/token');
+ $this->headers = [
+ '__RequestVerificationToken: '.$data['token'],
+ 'Content-Type: text/xml'
+ ];
+ }
+ $this->authorized = true;
+ }
+
+
+ // get* methods have to be protected for __call magic to work
+
+ protected function getDeviceInformation() {
+ return $this->request('device/information');
+ }
+
+ protected function getDeviceSignal() {
+ return $this->request('device/signal');
+ }
+
+ protected function getMonitoringStatus() {
+ return $this->request('monitoring/status');
+ }
+
+ protected function getNotifications() {
+ return $this->request('monitoring/check-notifications');
+ }
+
+ protected function getDialupConnection() {
+ return $this->request('dialup/connection');
+ }
+
+ protected function getTrafficStats() {
+ return $this->request('monitoring/traffic-statistics');
+ }
+
+ protected function getSMSCount() {
+ return $this->request('sms/sms-count');
+ }
+
+ protected function getSMSList($page = 1, $count = 20) {
+ $xml = $this->request('sms/sms-list', 'POST', [
+ 'PageIndex' => $page,
+ 'ReadCount' => $count,
+ 'BoxType' => 1,
+ 'SortType' => 0,
+ 'Ascending' => 0,
+ 'UnreadPreferred' => 1
+ ], true);
+ $xml = simplexml_load_string($xml);
+
+ $messages = [];
+ foreach ($xml->Messages->Message as $message) {
+ $messages[] = [
+ 'date' => (string)$message->Date,
+ 'phone' => (string)$message->Phone,
+ 'content' => (string)$message->Content
+ ];
+ }
+ return $messages;
+ }
+
+ private function xmlToAssoc(string $xml): array {
+ $xml = new SimpleXMLElement($xml);
+ $data = [];
+ foreach ($xml as $name => $value) {
+ $data[$name] = (string)$value;
+ }
+ return $data;
+ }
+
+ private function request(string $method, string $http_method = 'GET', array $data = [], bool $return_body = false) {
+ $ch = curl_init();
+ $url = 'http://'.$this->host.'/api/'.$method;
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ if (!empty($this->headers))
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
+ if ($http_method == 'POST') {
+ curl_setopt($ch, CURLOPT_POST, true);
+ if (!empty($data))
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $this->postDataToXML($data));
+ }
+ $body = curl_exec($ch);
+
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ if ($code != 200)
+ throw new Exception('e3372 host returned code '.$code);
+
+ curl_close($ch);
+ return $return_body ? $body : $this->xmlToAssoc($body);
+ }
+
+ private function postDataToXML(array $data, int $depth = 1) {
+ if ($depth == 1)
+ return '<?xml version: "1.0" encoding="UTF-8"?>'.$this->postDataToXML(['request' => $data], $depth+1);
+
+ $items = [];
+ foreach ($data as $key => $value) {
+ if (is_array($value))
+ $value = $this->postDataToXML($value, $depth+1);
+ $items[] = "<{$key}>{$value}</{$key}>";
+ }
+
+ return implode('', $items);
+ }
+
+ public static function getNetworkTypeLabel($type): string {
+ switch ((int)$type) {
+ case self::MACRO_NET_WORK_TYPE_NOSERVICE: return 'NOSERVICE';
+ case self::MACRO_NET_WORK_TYPE_GSM: return 'GSM';
+ case self::MACRO_NET_WORK_TYPE_GPRS: return 'GPRS';
+ case self::MACRO_NET_WORK_TYPE_EDGE: return 'EDGE';
+ case self::MACRO_NET_WORK_TYPE_WCDMA: return 'WCDMA';
+ case self::MACRO_NET_WORK_TYPE_HSDPA: return 'HSDPA';
+ case self::MACRO_NET_WORK_TYPE_HSUPA: return 'HSUPA';
+ case self::MACRO_NET_WORK_TYPE_HSPA: return 'HSPA';
+ case self::MACRO_NET_WORK_TYPE_TDSCDMA: return 'TDSCDMA';
+ case self::MACRO_NET_WORK_TYPE_HSPA_PLUS: return 'HSPA_PLUS';
+ case self::MACRO_NET_WORK_TYPE_EVDO_REV_0: return 'EVDO_REV_0';
+ case self::MACRO_NET_WORK_TYPE_EVDO_REV_A: return 'EVDO_REV_A';
+ case self::MACRO_NET_WORK_TYPE_EVDO_REV_B: return 'EVDO_REV_B';
+ case self::MACRO_NET_WORK_TYPE_1xRTT: return '1xRTT';
+ case self::MACRO_NET_WORK_TYPE_UMB: return 'UMB';
+ case self::MACRO_NET_WORK_TYPE_1xEVDV: return '1xEVDV';
+ case self::MACRO_NET_WORK_TYPE_3xRTT: return '3xRTT';
+ case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM: return 'HSPA_PLUS_64QAM';
+ case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO: return 'HSPA_PLUS_MIMO';
+ case self::MACRO_NET_WORK_TYPE_LTE: return 'LTE';
+ case self::MACRO_NET_WORK_TYPE_EX_NOSERVICE: return 'NOSERVICE';
+ case self::MACRO_NET_WORK_TYPE_EX_GSM: return 'GSM';
+ case self::MACRO_NET_WORK_TYPE_EX_GPRS: return 'GPRS';
+ case self::MACRO_NET_WORK_TYPE_EX_EDGE: return 'EDGE';
+ case self::MACRO_NET_WORK_TYPE_EX_IS95A: return 'IS95A';
+ case self::MACRO_NET_WORK_TYPE_EX_IS95B: return 'IS95B';
+ case self::MACRO_NET_WORK_TYPE_EX_CDMA_1x: return 'CDMA_1x';
+ case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_0: return 'EVDO_REV_0';
+ case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_A: return 'EVDO_REV_A';
+ case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_B: return 'EVDO_REV_B';
+ case self::MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x: return 'HYBRID_CDMA_1x';
+ case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0: return 'HYBRID_EVDO_REV_0';
+ case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A: return 'HYBRID_EVDO_REV_A';
+ case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B: return 'HYBRID_EVDO_REV_B';
+ case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0: return 'EHRPD_REL_0';
+ case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A: return 'EHRPD_REL_A';
+ case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B: return 'EHRPD_REL_B';
+ case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0: return 'HYBRID_EHRPD_REL_0';
+ case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A: return 'HYBRID_EHRPD_REL_A';
+ case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B: return 'HYBRID_EHRPD_REL_B';
+ case self::MACRO_NET_WORK_TYPE_EX_WCDMA: return 'WCDMA';
+ case self::MACRO_NET_WORK_TYPE_EX_HSDPA: return 'HSDPA';
+ case self::MACRO_NET_WORK_TYPE_EX_HSUPA: return 'HSUPA';
+ case self::MACRO_NET_WORK_TYPE_EX_HSPA: return 'HSPA';
+ case self::MACRO_NET_WORK_TYPE_EX_HSPA_PLUS: return 'HSPA_PLUS';
+ case self::MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS: return 'DC_HSPA_PLUS';
+ case self::MACRO_NET_WORK_TYPE_EX_TD_SCDMA: return 'TD_SCDMA';
+ case self::MACRO_NET_WORK_TYPE_EX_TD_HSDPA: return 'TD_HSDPA';
+ case self::MACRO_NET_WORK_TYPE_EX_TD_HSUPA: return 'TD_HSUPA';
+ case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA: return 'TD_HSPA';
+ case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS: return 'TD_HSPA_PLUS';
+ case self::MACRO_NET_WORK_TYPE_EX_802_16E: return '802_16E';
+ case self::MACRO_NET_WORK_TYPE_EX_LTE: return 'LTE';
+ default: return '?';
+ }
+ }
+
+}
diff --git a/localwebsite/classes/GPIORelaydClient.php b/localwebsite/classes/GPIORelaydClient.php
new file mode 100644
index 0000000..89c8dc9
--- /dev/null
+++ b/localwebsite/classes/GPIORelaydClient.php
@@ -0,0 +1,18 @@
+<?php
+
+class GPIORelaydClient extends MySimpleSocketClient {
+
+ const STATUS_ON = 'on';
+ const STATUS_OFF = 'off';
+
+ public function setStatus(string $status) {
+ $this->send($status);
+ return $this->recv();
+ }
+
+ public function getStatus() {
+ $this->send('get');
+ return $this->recv();
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/classes/InverterdClient.php b/localwebsite/classes/InverterdClient.php
new file mode 100644
index 0000000..b68b784
--- /dev/null
+++ b/localwebsite/classes/InverterdClient.php
@@ -0,0 +1,69 @@
+<?php
+
+class InverterdClient extends MySimpleSocketClient {
+
+ /**
+ * @throws Exception
+ */
+ public function setProtocol(int $v): string
+ {
+ $this->send("v $v");
+ return $this->recv();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function setFormat(string $fmt): string
+ {
+ $this->send("format $fmt");
+ return $this->recv();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function exec(string $command, array $arguments = []): string
+ {
+ $buf = "exec $command";
+ if (!empty($arguments)) {
+ foreach ($arguments as $arg)
+ $buf .= " $arg";
+ }
+ $this->send($buf);
+ return $this->recv();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function recv()
+ {
+ $recv_buf = '';
+ $buf = '';
+
+ while (true) {
+ $result = socket_recv($this->sock, $recv_buf, 1024, 0);
+ if ($result === false)
+ throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError());
+
+ // peer disconnected
+ if ($result === 0)
+ break;
+
+ $buf .= $recv_buf;
+ if (endsWith($buf, "\r\n\r\n"))
+ break;
+ }
+
+ $response = explode("\r\n", $buf);
+ $status = array_shift($response);
+ if (!in_array($status, ['ok', 'err']))
+ throw new Exception(__METHOD__.': unexpected status ('.$status.')');
+ if ($status == 'err')
+ throw new Exception(empty($response) ? 'unknown inverterd error' : $response[0]);
+
+ return trim(implode("\r\n", $response));
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/classes/MyOpenWrtUtils.php b/localwebsite/classes/MyOpenWrtUtils.php
new file mode 100644
index 0000000..6b70dbc
--- /dev/null
+++ b/localwebsite/classes/MyOpenWrtUtils.php
@@ -0,0 +1,133 @@
+<?php
+
+class MyOpenWrtUtils {
+
+ public static function getRoutingTable($table = null) {
+ $arguments = ['route-show'];
+ if ($table)
+ $arguments[] = $table;
+
+ return self::toList(self::run($arguments));
+ }
+
+ public static function getRoutingRules() {
+ return self::toList(self::run(['rule-show']));
+ }
+
+ public static function ipsetList($set_name) {
+ return self::toList(self::run(['ipset-list', $set_name]));
+ }
+
+ public static function ipsetListAll() {
+ global $config;
+
+ $args = ['ipset-list-all'];
+ $args = array_merge($args, array_keys($config['modems']));
+
+ $lines = self::toList(self::run($args));
+
+ $sets = [];
+ $cur_set = null;
+ foreach ($lines as $line) {
+ if (startsWith($line, '>')) {
+ $cur_set = substr($line, 1);
+ if (!isset($sets[$cur_set]))
+ $sets[$cur_set] = [];
+ continue;
+ }
+
+ if (is_null($cur_set)) {
+ debugError(__METHOD__.': cur_set is not set');
+ continue;
+ }
+
+ $sets[$cur_set][] = $line;
+ }
+
+ return $sets;
+ }
+
+ public static function ipsetAdd($set_name, $ip) {
+ return self::run(['ipset-add', $set_name, $ip]);
+ }
+
+ public static function ipsetDel($set_name, $ip) {
+ return self::run(['ipset-del', $set_name, $ip]);
+ }
+
+ public static function getDHCPLeases() {
+ $list = self::toList(self::run(['dhcp-leases']));
+ $list = array_map('self::toDHCPLease', $list);
+ return $list;
+ }
+
+
+ //
+ // http functions
+ //
+
+ private static function run(array $arguments) {
+ $url = self::getLink($arguments);
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ $body = curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ if ($code != 200)
+ throw new Exception(__METHOD__.': http code '.$code);
+
+ curl_close($ch);
+ return trim($body);
+ }
+
+ private static function getLink($arguments) {
+ global $config;
+
+ $url = 'http://'.$config['openwrt_ip'].'/cgi-bin/luci/command/cfg099944';
+ if (!empty($arguments)) {
+ $arguments = array_map(function($arg) {
+ $arg = str_replace('/', '_', $arg);
+ return urlencode($arg);
+ }, $arguments);
+ $arguments = implode('%20', $arguments);
+
+ $url .= '/';
+ $url .= $arguments;
+ }
+
+ // debugLog($url);
+
+ return $url;
+ }
+
+
+ //
+ // parsing functions
+ //
+
+ private static function toList(string $s): array {
+ if ($s == '')
+ return [];
+ return explode("\n", $s);
+ }
+
+ private static function toDHCPLease(string $s): array {
+ $words = explode(' ', $s);
+ $time = array_shift($words);
+ $mac = array_shift($words);
+ $ip = array_shift($words);
+ array_pop($words);
+ $hostname = trim(implode(' ', $words));
+ if (!$hostname)
+ $hostname = '?';
+ return [
+ 'time' => $time,
+ 'time_s' => date('d M, H:i:s', $time),
+ 'mac' => $mac,
+ 'ip' => $ip,
+ 'hostname' => $hostname
+ ];
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/classes/MySimpleSocketClient.php b/localwebsite/classes/MySimpleSocketClient.php
new file mode 100644
index 0000000..e59efba
--- /dev/null
+++ b/localwebsite/classes/MySimpleSocketClient.php
@@ -0,0 +1,90 @@
+<?php
+
+class MySimpleSocketClient {
+
+ protected $sock;
+
+ public function __construct(string $host, int $port)
+ {
+ if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false)
+ throw new Exception("socket_create() failed: ".$this->getSocketError());
+
+ $this->sock = $socket;
+
+ if ((socket_connect($socket, $host, $port)) === false)
+ throw new Exception("socket_connect() failed: ".$this->getSocketError());
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function send(string $data)
+ {
+ $data .= "\r\n";
+ $remained = strlen($data);
+
+ while ($remained > 0) {
+ $result = socket_write($this->sock, $data);
+ if ($result === false)
+ throw new Exception(__METHOD__ . ": socket_write() failed: ".$this->getSocketError());
+
+ $remained -= $result;
+ if ($remained > 0)
+ $data = substr($data, $result);
+ }
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function recv()
+ {
+ $recv_buf = '';
+ $buf = '';
+
+ while (true) {
+ $result = socket_recv($this->sock, $recv_buf, 1024, 0);
+ if ($result === false)
+ throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError());
+
+ // peer disconnected
+ if ($result === 0)
+ break;
+
+ $buf .= $recv_buf;
+ if (endsWith($buf, "\r\n"))
+ break;
+ }
+
+ return trim($buf);
+ }
+
+ /**
+ * Close connection.
+ */
+ public function close()
+ {
+ if (!$this->sock)
+ return;
+
+ socket_close($this->sock);
+ $this->sock = null;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getSocketError(): string
+ {
+ $sle_args = [];
+ if ($this->sock !== null)
+ $sle_args[] = $this->sock;
+ return socket_strerror(socket_last_error(...$sle_args));
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/classes/Si7021dClient.php b/localwebsite/classes/Si7021dClient.php
new file mode 100644
index 0000000..b976448
--- /dev/null
+++ b/localwebsite/classes/Si7021dClient.php
@@ -0,0 +1,31 @@
+<?php
+
+class Si7021dClient extends MySimpleSocketClient {
+
+ public string $name;
+ public float $temp;
+ public float $humidity;
+
+ /**
+ * @throws Exception
+ */
+ public function __construct(string $host, int $port, string $name) {
+ parent::__construct($host, $port);
+ $this->name = $name;
+
+ socket_set_timeout($this->sock, 3);
+ }
+
+ public function readSensor(): void {
+ $this->send('read');
+
+ $data = jsonDecode($this->recv());
+
+ $temp = round((float)$data['temp'], 3);
+ $hum = round((float)$data['humidity'], 3);
+
+ $this->temp = $temp;
+ $this->humidity = $hum;
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/composer.json b/localwebsite/composer.json
new file mode 100644
index 0000000..4df330c
--- /dev/null
+++ b/localwebsite/composer.json
@@ -0,0 +1,14 @@
+{
+ "name": "ch1p/localwebsite.homekit",
+ "type": "project",
+ "require": {
+ "twig/twig": "^3.3",
+ "ext-mbstring": "*",
+ "ext-sockets": "*",
+ "ext-simplexml": "*",
+ "ext-curl": "*",
+ "ext-json": "*",
+ "ext-gmp": "*"
+ },
+ "license": "MIT"
+}
diff --git a/localwebsite/composer.lock b/localwebsite/composer.lock
new file mode 100644
index 0000000..e5f3c15
--- /dev/null
+++ b/localwebsite/composer.lock
@@ -0,0 +1,205 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "d61e8a038cdf7b7e0b456809da3a1660",
+ "packages": [
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.23.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
+ "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "time": "2021-02-19T12:13:01+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.23.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+ "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2021-05-27T09:27:20+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "21578f00e83d4a82ecfa3d50752b609f13de6790"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/21578f00e83d4a82ecfa3d50752b609f13de6790",
+ "reference": "21578f00e83d4a82ecfa3d50752b609f13de6790",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3"
+ },
+ "require-dev": {
+ "psr/container": "^1.0",
+ "symfony/phpunit-bridge": "^4.4.9|^5.0.9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Twig Team",
+ "role": "Contributors"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "templating"
+ ],
+ "time": "2021-05-16T12:14:13+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": []
+}
diff --git a/localwebsite/config.php b/localwebsite/config.php
new file mode 100644
index 0000000..47be2f7
--- /dev/null
+++ b/localwebsite/config.php
@@ -0,0 +1,58 @@
+<?php
+
+return [
+ 'group' => 'www-data',
+ 'files_mode' => 0664,
+ 'dirs_mode' => 0775,
+ 'is_dev' => true,
+ 'static_public_path' => '/assets',
+
+ 'openwrt_ip' => '192.168.1.1',
+
+ 'inverterd_host' => '192.168.1.2',
+ 'inverterd_port' => 8305,
+
+ 'pump_host' => '192.168.1.2',
+ 'pump_port' => 8307,
+
+ 'si7021d_servers' => [
+ // fill here, example:
+ 'hall' => ['192.168.1.3', 8306, 'Big Hall'],
+ ],
+
+ // modem names (array keys) must match ipset names and
+ // routing table names on the openwrt router
+ //
+ // the order of the keys in the array must be the same
+ // as the order in which fwmark iptables rules are applied
+ 'modems' => [
+ 'modem-example' => [
+ 'ip' => '1.2.3.4',
+ 'label' => 'Modem Name',
+ 'short_label' => 'Mname',
+ 'legacy_token_auth' => false,
+ ],
+ ],
+
+ // 'routing_smallhome_ip' => 'fill_me',
+ // 'routing_default' => 'fill_me',
+
+ 'debug_backtrace' => true,
+ 'debug_file' => '.debug.log',
+
+ 'twig_cache' => true,
+ 'templates' => [
+ 'web' => [
+ 'root' => 'templates-web',
+ 'cache' => 'cache/templates-web',
+ ],
+ ],
+
+ 'static' => [
+ 'app.css' => 6,
+ 'app.js' => 1,
+ 'polyfills.js' => 1,
+ 'modem.js' => 1,
+ 'inverter.js' => 2,
+ ]
+];
diff --git a/localwebsite/engine/debug.php b/localwebsite/engine/debug.php
new file mode 100644
index 0000000..b1b959f
--- /dev/null
+++ b/localwebsite/engine/debug.php
@@ -0,0 +1,355 @@
+<?php
+
+// require_once 'engine/mysql.php';
+
+class debug {
+
+ protected static $Types = [
+ 1 => 'E_ERROR',
+ 2 => 'E_WARNING',
+ 4 => 'E_PARSE',
+ 8 => 'E_NOTICE',
+ 16 => 'E_CORE_ERROR',
+ 32 => 'E_CORE_WARNING',
+ 64 => 'E_COMPILE_ERROR',
+ 128 => 'E_COMPILE_WARNING',
+ 256 => 'E_USER_ERROR',
+ 512 => 'E_USER_WARNING',
+ 1024 => 'E_USER_NOTICE',
+ 2048 => 'E_STRICT',
+ 4096 => 'E_RECOVERABLE_ERROR',
+ 8192 => 'E_DEPRECATED',
+ 16384 => 'E_USER_DEPRECATED',
+ 32767 => 'E_ALL'
+ ];
+
+ const STORE_NONE = -1;
+ const STORE_MYSQL = 0;
+ const STORE_FILE = 1;
+ const STORE_BOTH = 2;
+
+ private static $instance = null;
+
+ protected $enabled = false;
+ protected $errCounter = 0;
+ protected $logCounter = 0;
+ protected $messagesStoreType = self::STORE_NONE;
+ protected $errorsStoreType = self::STORE_NONE;
+ protected $filter;
+ protected $reportRecursionLevel = 0;
+ protected $overridenDebugFile = null;
+ protected $silent = false;
+ protected $prefix;
+
+ private function __construct($filter) {
+ $this->filter = $filter;
+ }
+
+ public static function getInstance($filter = null) {
+ if (is_null(self::$instance)) {
+ self::$instance = new self($filter);
+ }
+ return self::$instance;
+ }
+
+ public function enable() {
+ $self = $this;
+
+ set_error_handler(function($no, $str, $file, $line) use ($self) {
+ if ($self->silent || !$self->enabled) {
+ return;
+ }
+ if ((is_callable($this->filter) && !($this->filter)($no, $file, $line, $str)) || !$self->canReport()) {
+ return;
+ }
+ $self->report(true, $str, $no, $file, $line);
+ });
+
+ append_shutdown_function(function() use ($self) {
+ if (!$self->enabled || !($error = error_get_last())) {
+ return;
+ }
+ if (is_callable($this->filter)
+ && !($this->filter)($error['type'], $error['file'], $error['line'], $error['message'])) {
+ return;
+ }
+ if (!$self->canReport()) {
+ return;
+ }
+ $self->report(true, $error['message'], $error['type'], $error['file'], $error['line']);
+ });
+
+ $this->enabled = true;
+ }
+
+ public function disable() {
+ restore_error_handler();
+ $this->enabled = false;
+ }
+
+ public function report($is_error, $text, $errno = 0, $errfile = '', $errline = '') {
+ global $config;
+
+ $this->reportRecursionLevel++;
+
+ $logstarted = $this->errCounter > 0 || $this->logCounter > 0;
+ $num = $is_error ? $this->errCounter++ : $this->logCounter++;
+ $custom = $is_error && !$errno;
+ $ts = time();
+ $exectime = exectime();
+ $bt = backtrace(2);
+
+ $store_file = (!$is_error && $this->checkMessagesStoreType(self::STORE_FILE))
+ || ($is_error && $this->checkErrorsStoreType(self::STORE_FILE));
+
+ $store_mysql = (!$is_error && $this->checkMessagesStoreType(self::STORE_MYSQL))
+ || ($is_error && $this->checkErrorsStoreType(self::STORE_MYSQL));
+
+ if ($this->prefix)
+ $text = $this->prefix.$text;
+
+ // if ($store_mysql) {
+ // $db = getMySQL('local_logs', true);
+ // $data = [
+ // 'ts' => $ts,
+ // 'num' => $num,
+ // 'time' => $exectime,
+ // 'custom' => intval($custom),
+ // 'errno' => $errno,
+ // 'file' => $errfile,
+ // 'line' => $errline,
+ // 'text' => $text,
+ // 'stacktrace' => $bt,
+ // 'is_cli' => PHP_SAPI == 'cli' ? 1 : 0,
+ // ];
+ // if (PHP_SAPI == 'cli') {
+ // $data += [
+ // 'ip' => '',
+ // 'ua' => '',
+ // 'url' => '',
+ // ];
+ // } else {
+ // $data += [
+ // 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']),
+ // 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ // 'url' => $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']
+ // ];
+ // }
+ // $db->insert('backend_errors', $data);
+ // }
+
+ if ($store_file) {
+ $title = PHP_SAPI == 'cli' ? 'cli' : $_SERVER['REQUEST_URI'];
+ $date = date('d/m/y H:i:s', $ts);
+ $exectime = (string)$exectime;
+ if (strlen($exectime) < 6)
+ $exectime .= str_repeat('0', 6 - strlen($exectime));
+
+ $buf = "";
+ if (!$logstarted) {
+ $buf .= "\n<e fg=white bg=magenta style=fgbright,bold> {$title} </e><e fg=white bg=blue style=fgbright> {$date} </e>\n";
+ }
+ $buf .= "<e fg=".($is_error ? 'red' : 'white').">".($is_error ? 'E' : 'I')."=<e style=bold>${num}</e> <e fg=cyan>{$exectime}</e> ";
+ if ($is_error && !$custom) {
+ $buf .= "<e fg=green>{$errfile}<e fg=white>:<e fg=green style=fgbright>{$errline}</e> (".self::errname($errno).") ";
+ }
+ $buf = stransi($buf);
+
+ $buf .= $text;
+ $buf .= "\n";
+ if ($is_error && $config['debug_backtrace']) {
+ $buf .= $bt."\n";
+ }
+
+ $debug_file = $this->getDebugFile();
+
+ $logdir = dirname($debug_file);
+ if (!file_exists($logdir)) {
+ mkdir($logdir);
+ setperm($logdir);
+ }
+
+ $f = fopen($debug_file, 'a');
+ if ($f) {
+ fwrite($f, $buf);
+ fclose($f);
+ }
+ }
+
+ $this->reportRecursionLevel--;
+ }
+
+ public function canReport() {
+ return $this->reportRecursionLevel < 2;
+ }
+
+ public function setErrorsStoreType($errorsStoreType) {
+ $this->errorsStoreType = $errorsStoreType;
+ }
+
+ public function setMessagesStoreType($messagesStoreType) {
+ $this->messagesStoreType = $messagesStoreType;
+ }
+
+ public function checkMessagesStoreType($store_type) {
+ return $this->messagesStoreType == $store_type || $this->messagesStoreType == self::STORE_BOTH;
+ }
+
+ public function checkErrorsStoreType($store_type) {
+ return $this->errorsStoreType == $store_type || $this->errorsStoreType == self::STORE_BOTH;
+ }
+
+ public function overrideDebugFile($file) {
+ $this->overridenDebugFile = $file;
+ }
+
+ protected function getDebugFile() {
+ global $config;
+ return is_null($this->overridenDebugFile) ? ROOT.'/'.$config['debug_file'] : $this->overridenDebugFile;
+ }
+
+ public function setSilence($silent) {
+ $this->silent = $silent;
+ }
+
+ public function setPrefix($prefix) {
+ $this->prefix = $prefix;
+ }
+
+ public static function errname($errno) {
+ static $errors = null;
+ if (is_null($errors)) {
+ $errors = array_flip(array_slice(get_defined_constants(true)['Core'], 0, 15, true));
+ }
+ return $errors[$errno];
+ }
+
+ public static function getTypes() {
+ return self::$Types;
+ }
+
+}
+
+class debug_measure {
+
+ private $name;
+ private $time;
+ private $started = false;
+
+ /**
+ * @param string $name
+ * @return $this
+ */
+ public function start($name = null) {
+ if (is_null($name)) {
+ $name = strgen(3);
+ }
+ $this->name = $name;
+ $this->time = microtime(true);
+ $this->started = true;
+ return $this;
+ }
+
+ /**
+ * @return float|string|null
+ */
+ public function finish() {
+ if (!$this->started) {
+ debugLog("debug_measure::finish(): not started, name=".$this->name);
+ return null;
+ }
+
+ $time = (microtime(true) - $this->time);
+ debugLog("MEASURE".($this->name != '' ? ' '.$this->name : '').": ".$time);
+
+ $this->started = false;
+ return $time;
+ }
+
+}
+
+/**
+ * @param $var
+ * @return string
+ */
+function str_print_r($var) {
+ ob_start();
+ print_r($var);
+ return trim(ob_get_clean());
+}
+
+/**
+ * @param $var
+ * @return string
+ */
+function str_var_dump($var) {
+ ob_start();
+ var_dump($var);
+ return trim(ob_get_clean());
+}
+
+/**
+ * @param $args
+ * @param bool $all_dump
+ * @return string
+ */
+function str_vars($args, $all_dump = false) {
+ return implode(' ', array_map(function($a) use ($all_dump) {
+ if ($all_dump) {
+ return str_var_dump($a);
+ }
+ $type = gettype($a);
+ if ($type == 'string' || $type == 'integer' || $type == 'double') {
+ return $a;
+ } else if ($type == 'array' || $type == 'object') {
+ return str_print_r($a);
+ } else {
+ return str_var_dump($a);
+ }
+ }, $args));
+}
+
+/**
+ * @param int $shift
+ * @return string
+ */
+function backtrace($shift = 0) {
+ $bt = debug_backtrace();
+ $lines = [];
+ foreach ($bt as $i => $t) {
+ if ($i < $shift) {
+ continue;
+ }
+ if (!isset($t['file'])) {
+ $lines[] = 'from ?';
+ } else {
+ $lines[] = 'from '.$t['file'].':'.$t['line'];
+ }
+ }
+ return implode("\n", $lines);
+}
+
+/**
+ * @param mixed ...$args
+ */
+function debugLog(...$args) {
+ global $config;
+ if (!$config['is_dev'])
+ return;
+
+ debug::getInstance()->report(false, str_vars($args));
+}
+
+function debugLogOnProd(...$args) {
+ debug::getInstance()->report(false, str_vars($args));
+}
+
+/**
+ * @param mixed ...$args
+ */
+function debugError(...$args) {
+ $debug = debug::getInstance();
+ if ($debug->canReport()) {
+ $debug->report(true, str_vars($args));
+ }
+}
diff --git a/localwebsite/engine/request_handler.php b/localwebsite/engine/request_handler.php
new file mode 100644
index 0000000..535e850
--- /dev/null
+++ b/localwebsite/engine/request_handler.php
@@ -0,0 +1,142 @@
+<?php
+
+abstract class request_handler {
+
+ const GET = 'GET';
+ const POST = 'POST';
+
+ private static array $AllowedInputTypes = ['i', 'f', 'b', 'e' /* enum */];
+
+ public function dispatch(string $act) {
+ $method = $_SERVER['REQUEST_METHOD'] == 'POST' ? 'POST' : 'GET';
+ return $this->call_act($method, $act);
+ }
+
+ protected function before_dispatch(string $method, string $act)/*: ?array*/ {
+ return null;
+ }
+
+ protected function call_act(string $method, string $act, array $input = []) {
+ global $RouterInput;
+
+ $notfound = !method_exists($this, $method.'_'.$act) || !((new ReflectionMethod($this, $method.'_'.$act))->isPublic());
+ if ($notfound)
+ $this->method_not_found($method, $act);
+
+ if (!empty($input)) {
+ foreach ($input as $k => $v)
+ $RouterInput[$k] = $v;
+ }
+
+ $args = $this->before_dispatch($method, $act);
+ return call_user_func_array([$this, $method.'_'.$act], is_array($args) ? [$args] : []);
+ }
+
+ abstract protected function method_not_found(string $method, string $act);
+
+ protected function input(string $input, bool $as_assoc = false): array {
+ $input = preg_split('/,\s+?/', $input, null, PREG_SPLIT_NO_EMPTY);
+
+ $ret = [];
+ foreach ($input as $var) {
+ list($type, $name, $enum_values, $enum_default) = self::parse_input_var($var);
+
+ $value = param($name);
+
+ switch ($type) {
+ case 'i':
+ if (is_null($value) && !is_null($enum_default)) {
+ $value = (int)$enum_default;
+ } else {
+ $value = (int)$value;
+ }
+ break;
+
+ case 'f':
+ if (is_null($value) && !is_null($enum_default)) {
+ $value = (float)$enum_default;
+ } else {
+ $value = (float)$value;
+ }
+ break;
+
+ case 'b':
+ if (is_null($value) && !is_null($enum_default)) {
+ $value = (bool)$enum_default;
+ } else {
+ $value = (bool)$value;
+ }
+ break;
+
+ case 'e':
+ if (!in_array($value, $enum_values)) {
+ $value = !is_null($enum_default) ? $enum_default : '';
+ }
+ break;
+ }
+
+ if (!$as_assoc) {
+ $ret[] = $value;
+ } else {
+ $ret[$name] = $value;
+ }
+ }
+
+ return $ret;
+ }
+ protected static function parse_input_var(string $var): array {
+ $type = null;
+ $name = null;
+ $enum_values = null;
+ $enum_default = null;
+
+ $pos = strpos($var, ':');
+ if ($pos !== false) {
+ $type = substr($var, 0, $pos);
+ $rest = substr($var, $pos+1);
+
+ if (!in_array($type, self::$AllowedInputTypes)) {
+ trigger_error('request_handler::parse_input_var('.$var.'): unknown type '.$type);
+ $type = null;
+ }
+
+ switch ($type) {
+ case 'e':
+ $br_from = strpos($rest, '(');
+ $br_to = strpos($rest, ')');
+
+ if ($br_from === false || $br_to === false) {
+ trigger_error('request_handler::parse_input_var('.$var.'): failed to parse enum values');
+ $type = null;
+ $name = $rest;
+ break;
+ }
+
+ $enum_values = array_map('trim', explode('|', trim(substr($rest, $br_from+1, $br_to-$br_from-1))));
+ $name = trim(substr($rest, 0, $br_from));
+
+ if (!empty($enum_values)) foreach ($enum_values as $key => $val) {
+ if (substr($val, 0, 1) == '=') {
+ $enum_values[$key] = substr($val, 1);
+ $enum_default = $enum_values[$key];
+ }
+ }
+ break;
+
+ default:
+ if (($eq_pos = strpos($rest, '=')) !== false) {
+ $enum_default = substr($rest, $eq_pos+1);
+ $rest = substr($rest, 0, $eq_pos);
+ }
+ $name = trim($rest);
+ break;
+ }
+ } else {
+ $type = 's';
+ $name = $var;
+ }
+
+ return [$type, $name, $enum_values, $enum_default];
+ }
+
+}
diff --git a/localwebsite/engine/router.php b/localwebsite/engine/router.php
new file mode 100644
index 0000000..5e966a9
--- /dev/null
+++ b/localwebsite/engine/router.php
@@ -0,0 +1,199 @@
+<?php
+
+class router {
+
+ protected array $routes = [
+ 'children' => [],
+ 're_children' => []
+ ];
+
+ public function add($template, $value) {
+ if ($template == '') {
+ return;
+ }
+
+ // expand {enum,erat,ions}
+ $templates = [[$template, $value]];
+ if (preg_match_all('/\{([\w\d_\-,]+)\}/', $template, $matches)) {
+ foreach ($matches[1] as $match_index => $variants) {
+ $variants = explode(',', $variants);
+ $variants = array_map('trim', $variants);
+ $variants = array_filter($variants, function($s) { return $s != ''; });
+
+ for ($i = 0; $i < count($templates); ) {
+ list($template, $value) = $templates[$i];
+ $new_templates = [];
+ foreach ($variants as $variant_index => $variant) {
+ $new_templates[] = [
+ str_replace_once($matches[0][$match_index], $variant, $template),
+ str_replace('${'.($match_index+1).'}', $variant, $value)
+ ];
+ }
+ array_splice($templates, $i, 1, $new_templates);
+ $i += count($new_templates);
+ }
+ }
+ }
+
+ // process all generated routes
+ foreach ($templates as $template) {
+ list($template, $value) = $template;
+
+ $start_pos = 0;
+ $parent = &$this->routes;
+ $template_len = strlen($template);
+
+ while ($start_pos < $template_len) {
+ $slash_pos = strpos($template, '/', $start_pos);
+ if ($slash_pos !== false) {
+ $part = substr($template, $start_pos, $slash_pos-$start_pos+1);
+ $start_pos = $slash_pos+1;
+ } else {
+ $part = substr($template, $start_pos);
+ $start_pos = $template_len;
+ }
+
+ $parent = &$this->_addRoute($parent, $part,
+ $start_pos < $template_len ? null : $value);
+ }
+ }
+ }
+
+ protected function &_addRoute(&$parent, $part, $value = null) {
+ $par_pos = strpos($part, '(');
+ $is_regex = $par_pos !== false && ($par_pos == 0 || $part[$par_pos-1] != '\\');
+
+ $children_key = !$is_regex ? 'children' : 're_children';
+
+ if (isset($parent[$children_key][$part])) {
+ if (is_null($value)) {
+ $parent = &$parent[$children_key][$part];
+ } else {
+ if (!isset($parent[$children_key][$part]['value'])) {
+ $parent[$children_key][$part]['value'] = $value;
+ } else {
+ trigger_error(__METHOD__.': route is already defined');
+ }
+ }
+ return $parent;
+ }
+
+ $child = [
+ 'children' => [],
+ 're_children' => []
+ ];
+ if (!is_null($value)) {
+ $child['value'] = $value;
+ }
+
+ $parent[$children_key][$part] = $child;
+ return $parent[$children_key][$part];
+ }
+
+ public function find($uri) {
+ if ($uri != '/' && $uri[0] == '/') {
+ $uri = substr($uri, 1);
+ }
+ $start_pos = 0;
+ $parent = &$this->routes;
+ $uri_len = strlen($uri);
+ $matches = [];
+
+ while ($start_pos < $uri_len) {
+ $slash_pos = strpos($uri, '/', $start_pos);
+ if ($slash_pos !== false) {
+ $part = substr($uri, $start_pos, $slash_pos-$start_pos+1);
+ $start_pos = $slash_pos+1;
+ } else {
+ $part = substr($uri, $start_pos);
+ $start_pos = $uri_len;
+ }
+
+ $found = false;
+ if (isset($parent['children'][$part])) {
+ $parent = &$parent['children'][$part];
+ $found = true;
+ } else if (!empty($parent['re_children'])) {
+ foreach ($parent['re_children'] as $re => &$child) {
+ $exp = '#^'.$re.'$#';
+ $re_result = preg_match($exp, $part, $match);
+ if ($re_result === false) {
+ debugError(__METHOD__.": regex $exp failed");
+ continue;
+ }
+
+ if ($re_result) {
+ if (count($match) > 1) {
+ $matches = array_merge($matches, array_slice($match, 1));
+ }
+ $parent = &$child;
+ $found = true;
+ break;
+ }
+ }
+ }
+
+ if (!$found) {
+ return false;
+ }
+ }
+
+ if (!isset($parent['value'])) {
+ return false;
+ }
+
+ $value = $parent['value'];
+ if (!empty($matches)) {
+ foreach ($matches as $i => $match) {
+ $needle = '$('.($i+1).')';
+ $pos = strpos($value, $needle);
+ if ($pos !== false) {
+ $value = substr_replace($value, $match, $pos, strlen($needle));
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ public function load($routes) {
+ $this->routes = $routes;
+ }
+
+ public function dump() {
+ return $this->routes;
+ }
+
+}
+
+function routerFind(router $router) {
+ $document_uri = $_SERVER['REQUEST_URI'];
+ if (($pos = strpos($document_uri, '?')) !== false)
+ $document_uri = substr($document_uri, 0, $pos);
+ $document_uri = urldecode($document_uri);
+
+ $fixed_document_uri = preg_replace('#/+#', '/', $document_uri);
+ if ($fixed_document_uri != $document_uri && !is_xhr_request()) {
+ redirect($fixed_document_uri);
+ } else {
+ $document_uri = $fixed_document_uri;
+ }
+
+ $route = $router->find($document_uri);
+ if ($route === false)
+ return false;
+
+ $route = preg_split('/ +/', $route);
+ $handler = $route[0];
+ $act = $route[1];
+ $input = [];
+ if (count($route) > 2) {
+ for ($i = 2; $i < count($route); $i++) {
+ $var = $route[$i];
+ list($k, $v) = explode('=', $var);
+ $input[trim($k)] = trim($v);
+ }
+ }
+
+ return [$handler, $act, $input];
+} \ No newline at end of file
diff --git a/localwebsite/engine/tpl.php b/localwebsite/engine/tpl.php
new file mode 100644
index 0000000..a12807d
--- /dev/null
+++ b/localwebsite/engine/tpl.php
@@ -0,0 +1,520 @@
+<?php
+
+abstract class base_tpl {
+
+ public $twig;
+ protected $vars = [];
+ protected $global_vars = [];
+ protected $title = '';
+ protected $title_modifiers = [];
+ protected $keywords = '';
+ protected $description = '';
+ protected $js = [];
+ protected $lang_keys = [];
+ protected $static = [];
+ protected $external_static = [];
+ protected $head = [];
+ protected $globals_applied = false;
+ protected $static_time;
+
+ public function __construct($templates_dir, $cache_dir) {
+ global $config;
+
+ $cl = get_called_class();
+
+ $this->twig = self::twig_instance($templates_dir, $cache_dir, $config['is_dev']);
+ $this->static_time = time();
+ }
+
+ public static function twig_instance($templates_dir, $cache_dir, $auto_reload) {
+ // must specify a second argument ($rootPath) here
+ // otherwise it will be getcwd() and it's www-prod/htdocs/ for apache and www-prod/ for cli code
+ // this is bad for templates rebuilding
+ $twig_loader = new \Twig\Loader\FilesystemLoader($templates_dir, ROOT);
+
+ $env_options = [];
+ if (!is_null($cache_dir)) {
+ $env_options += [
+ 'cache' => $cache_dir,
+ 'auto_reload' => $auto_reload
+ ];
+ }
+
+ $twig = new \Twig\Environment($twig_loader, $env_options);
+ $twig->addExtension(new Twig_MyExtension);
+
+ return $twig;
+ }
+
+ public function render($template, array $vars = []) {
+ $this->apply_globals();
+ return $this->do_render($template, array_merge($this->vars, $vars));
+ }
+
+ protected function do_render($template, $vars) {
+ global $config;
+ $s = '';
+ try {
+ $s = $this->twig->render($template, $vars);
+ } catch (\Twig\Error\Error $e) {
+ $error = get_class($e).": failed to render";
+ $source_ctx = $e->getSourceContext();
+ if ($source_ctx) {
+ $path = $source_ctx->getPath();
+ if (startsWith($path, ROOT))
+ $path = substr($path, strlen(ROOT)+1);
+ $error .= " ".$source_ctx->getName()." (".$path.") at line ".$e->getTemplateLine();
+ }
+ $error .= ": ";
+ $error .= $e->getMessage();
+ debugError($error);
+ if ($config['is_dev'])
+ $s = $error."\n";
+ }
+ return $s;
+ }
+
+ public function set($arg1, $arg2 = null) {
+ if (is_array($arg1)) {
+ foreach ($arg1 as $key => $value) {
+ $this->vars[$key] = $value;
+ }
+ } elseif ($arg2 !== null) {
+ $this->vars[$arg1] = $arg2;
+ }
+ }
+
+ public function is_set($key): bool {
+ return isset($this->vars[$key]);
+ }
+
+ public function set_global($arg1, $arg2 = null) {
+ if (is_array($arg1)) {
+ foreach ($arg1 as $key => $value) {
+ $this->global_vars[$key] = $value;
+ }
+ } elseif ($arg2 !== null) {
+ $this->global_vars[$arg1] = $arg2;
+ }
+ }
+
+ public function is_global_set($key): bool {
+ return isset($this->global_vars[$key]);
+ }
+
+ public function get_global($key) {
+ return $this->is_global_set($key) ? $this->global_vars[$key] : null;
+ }
+
+ public function apply_globals() {
+ if (!empty($this->global_vars) && !$this->globals_applied) {
+ foreach ($this->global_vars as $key => $value)
+ $this->twig->addGlobal($key, $value);
+ $this->globals_applied = true;
+ }
+ }
+
+ /**
+ * @param string $title
+ */
+ public function set_title($title) {
+ $this->title = $title;
+ }
+
+ /**
+ * @return string
+ */
+ public function get_title() {
+ $title = $this->title != '' ? $this->title : 'Домашний сайт';
+ if (!empty($this->title_modifiers)) {
+ foreach ($this->title_modifiers as $modifier) {
+ $title = $modifier($title);
+ }
+ }
+ return $title;
+ }
+
+ /**
+ * @param callable $callable
+ */
+ public function add_page_title_modifier(callable $callable) {
+ if (!is_callable($callable)) {
+ trigger_error(__METHOD__.': argument is not callable');
+ } else {
+ $this->title_modifiers[] = $callable;
+ }
+ }
+
+ /**
+ * @param string $css_name
+ * @param null $extra
+ */
+ public function add_static(string $name, $extra = null) {
+ global $config;
+ // $is_css = endsWith($name, '.css');
+ $this->static[] = [$name, $extra];
+ }
+
+ public function add_external_static($type, $url) {
+ $this->external_static[] = ['type' => $type, 'url' => $url];
+ }
+
+ public function add_js($js) {
+ $this->js[] = $js;
+ }
+
+ public function add_lang_keys(array $keys) {
+ $this->lang_keys = array_merge($this->lang_keys, $keys);
+ }
+
+ public function add_head($html) {
+ $this->head[] = $html;
+ }
+
+ public function get_head_html() {
+ global $config;
+ $lines = [];
+ $public_path = $config['static_public_path'];
+ foreach ($this->static as $val) {
+ list($name, $extra) = $val;
+ if (endsWith($name, '.js'))
+ $lines[] = self::js_link($public_path.'/'.$name, $config['static'][$name] ?? 1);
+ else
+ $lines[] = self::css_link($public_path.'/'.$name, $config['static'][$name] ?? 1, $extra);
+ }
+ if (!empty($this->external_static)) {
+ foreach ($this->external_static as $ext) {
+ if ($ext['type'] == 'js')
+ $lines[] = self::js_link($ext['url']);
+ else if ($ext['type'] == 'css')
+ $lines[] = self::css_link($ext['url']);
+ }
+ }
+ if (!empty($this->head)) {
+ $lines = array_merge($lines, $this->head);
+ }
+ return implode("\n", $lines);
+ }
+
+ public static function js_link($name, $version = null): string {
+ if ($version !== null)
+ $name .= '?'.$version;
+ return '<script src="'.$name.'" type="text/javascript"></script>';
+ }
+
+ public static function css_link($name, $version = null, $extra = null) {
+ if ($version !== null)
+ $name .= '?'.$version;
+ $s = '<link';
+ if (is_array($extra)) {
+ if (!empty($extra['id']))
+ $s .= ' id="'.$extra['id'].'"';
+ }
+ $s .= ' rel="stylesheet" type="text/css"';
+ if (is_array($extra) && !empty($extra['media']))
+ $s .= ' media="'.$extra['media'].'"';
+ $s .= ' href="'.$name.'"';
+ $s .= '>';
+ return $s;
+ }
+
+ public function get_lang_keys() {
+ global $lang;
+ $keys = [];
+ if (!empty($this->lang_keys)) {
+ foreach ($this->lang_keys as $key)
+ $keys[$key] = $lang[$key];
+ }
+ return $keys;
+ }
+
+ public function render_not_found() {
+ http_response_code(404);
+ if (!is_xhr_request()) {
+ $this->render_page('404.twig');
+ } else {
+ ajax_error(['code' => 404]);
+ }
+ }
+
+ /**
+ * @param null|string $reason
+ */
+ public function render_forbidden($reason = null) {
+ http_response_code(403);
+ if (!is_xhr_request()) {
+ $this->set(['reason' => $reason]);
+ $this->render_page('403.twig');
+ } else {
+ $data = ['code' => 403];
+ if (!is_null($reason))
+ $data['reason'] = $reason;
+ ajax_error($data);
+ }
+ }
+
+ public function must_revalidate() {
+ header('Cache-Control: no-store, no-cache, must-revalidate');
+ }
+
+ abstract public function render_page($template);
+
+}
+
+class web_tpl extends base_tpl {
+
+ protected $alternate = false;
+
+ public function __construct() {
+ global $config;
+ $templates = $config['templates']['web'];
+ parent::__construct(
+ ROOT.'/'. $templates['root'],
+ $config['twig_cache']
+ ? ROOT.'/'.$templates['cache']
+ : null
+ );
+ }
+
+ public function set_alternate($alt) {
+ $this->alternate = $alt;
+ }
+
+ public function render_page($template) {
+ echo $this->_render_header();
+ echo $this->_render_body($template);
+ echo $this->_render_footer();
+ exit;
+ }
+
+ public function _render_header() {
+ global $config;
+ $this->apply_globals();
+
+ $vars = [
+ 'title' => $this->get_title(),
+ 'keywords' => $this->keywords,
+ 'description' => $this->description,
+ 'alternate' => $this->alternate,
+ 'static' => $this->get_head_html(),
+ ];
+ return $this->do_render('header.twig', $vars);
+ }
+
+ public function _render_body($template) {
+ return $this->do_render($template, $this->vars);
+ }
+
+ public function _render_footer() {
+ $exec_time = microtime(true) - START_TIME;
+ $exec_time = round($exec_time, 4);
+
+ $footer_vars = [
+ 'exec_time' => $exec_time,
+ 'js' => !empty($this->js) ? implode("\n", $this->js) : '',
+ ];
+ return $this->do_render('footer.twig', $footer_vars);
+ }
+
+}
+
+class Twig_MyExtension extends \Twig\Extension\AbstractExtension {
+
+ public function getFilters() {
+ global $lang;
+
+ return array(
+ new \Twig\TwigFilter('lang', 'lang'),
+
+ new \Twig\TwigFilter('lang', function($key, array $args = []) use (&$lang) {
+ array_walk($args, function(&$item, $key) {
+ $item = htmlescape($item);
+ });
+ array_unshift($args, $key);
+ return call_user_func_array([$lang, 'get'], $args);
+ }, ['is_variadic' => true]),
+
+ new \Twig\TwigFilter('plural', function($text, array $args = []) use (&$lang) {
+ array_unshift($args, $text);
+ return call_user_func_array([$lang, 'num'], $args);
+ }, ['is_variadic' => true]),
+
+ new \Twig\TwigFilter('format_number', function($number, array $args = []) {
+ array_unshift($args, $number);
+ return call_user_func_array('formatNumber', $args);
+ }, ['is_variadic' => true]),
+
+ new \Twig\TwigFilter('short_number', function($number, array $args = []) {
+ array_unshift($args, $number);
+ return call_user_func_array('shortNumber', $args);
+ }, ['is_variadic']),
+
+ new \Twig\TwigFilter('format_time', function($ts, array $args = []) {
+ array_unshift($args, $ts);
+ return call_user_func_array('formatTime', $args);
+ }, ['is_variadic' => true]),
+
+ new \Twig\TwigFilter('format_duration', function($seconds, array $args = []) {
+ array_unshift($args, $seconds);
+ return call_user_func_array('formatDuration', $args);
+ }, ['is_variadic' => true]),
+ );
+ }
+
+ public function getTokenParsers() {
+ return [new JsTagTokenParser()];
+ }
+
+ public function getName() {
+ return 'lang';
+ }
+
+}
+
+// Based on https://stackoverflow.com/questions/26170727/how-to-create-a-twig-custom-tag-that-executes-a-callback
+class JsTagTokenParser extends \Twig\TokenParser\AbstractTokenParser {
+
+ public function parse(\Twig\Token $token) {
+ $lineno = $token->getLine();
+ $stream = $this->parser->getStream();
+
+ // recovers all inline parameters close to your tag name
+ $params = array_merge([], $this->getInlineParams($token));
+
+ $continue = true;
+ while ($continue) {
+ // create subtree until the decideJsTagFork() callback returns true
+ $body = $this->parser->subparse(array ($this, 'decideJsTagFork'));
+
+ // I like to put a switch here, in case you need to add middle tags, such
+ // as: {% js %}, {% nextjs %}, {% endjs %}.
+ $tag = $stream->next()->getValue();
+ switch ($tag) {
+ case 'endjs':
+ $continue = false;
+ break;
+ default:
+ throw new \Twig\Error\SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "endjs" to close the "mytag" block started at line %d)', $lineno), -1);
+ }
+
+ // you want $body at the beginning of your arguments
+ array_unshift($params, $body);
+
+ // if your endjs can also contains params, you can uncomment this line:
+ // $params = array_merge($params, $this->getInlineParams($token));
+ // and comment this one:
+ $stream->expect(\Twig\Token::BLOCK_END_TYPE);
+ }
+
+ return new JsTagNode(new \Twig\Node\Node($params), $lineno, $this->getTag());
+ }
+
+ /**
+ * Recovers all tag parameters until we find a BLOCK_END_TYPE ( %} )
+ *
+ * @param \Twig\Token $token
+ * @return array
+ */
+ protected function getInlineParams(\Twig\Token $token) {
+ $stream = $this->parser->getStream();
+ $params = array ();
+ while (!$stream->test(\Twig\Token::BLOCK_END_TYPE)) {
+ $params[] = $this->parser->getExpressionParser()->parseExpression();
+ }
+ $stream->expect(\Twig\Token::BLOCK_END_TYPE);
+ return $params;
+ }
+
+ /**
+ * Callback called at each tag name when subparsing, must return
+ * true when the expected end tag is reached.
+ *
+ * @param \Twig\Token $token
+ * @return bool
+ */
+ public function decideJsTagFork(\Twig\Token $token) {
+ return $token->test(['endjs']);
+ }
+
+ /**
+ * Your tag name: if the parsed tag match the one you put here, your parse()
+ * method will be called.
+ *
+ * @return string
+ */
+ public function getTag() {
+ return 'js';
+ }
+
+}
+
+class JsTagNode extends \Twig\Node\Node {
+
+ public function __construct($params, $lineno = 0, $tag = null) {
+ parent::__construct(['params' => $params], [], $lineno, $tag);
+ }
+
+ public function compile(\Twig\Compiler $compiler) {
+ $count = count($this->getNode('params'));
+
+ $compiler->addDebugInfo($this);
+ $compiler
+ ->write('global $__tpl;')
+ ->raw(PHP_EOL);
+
+ for ($i = 0; ($i < $count); $i++) {
+ // argument is not an expression (such as, a \Twig\Node\Textbody)
+ // we should trick with output buffering to get a valid argument to pass
+ // to the functionToCall() function.
+ if (!($this->getNode('params')->getNode($i) instanceof \Twig\Node\Expression\AbstractExpression)) {
+ $compiler
+ ->write('ob_start();')
+ ->raw(PHP_EOL);
+
+ $compiler
+ ->subcompile($this->getNode('params')->getNode($i));
+
+ $compiler
+ ->write('$js = ob_get_clean();')
+ ->raw(PHP_EOL);
+ }
+ }
+
+ $compiler
+ ->write('$__tpl->add_js($js);')
+ ->raw(PHP_EOL)
+ ->write('unset($js);')
+ ->raw(PHP_EOL);
+ }
+
+}
+
+
+
+/**
+ * @param $data
+ */
+function ajax_ok($data) {
+ ajax_response(['response' => $data]);
+}
+
+/**
+ * @param $error
+ * @param int $code
+ */
+function ajax_error($error, $code = 200) {
+ ajax_response(['error' => $error], $code);
+}
+
+/**
+ * @param $data
+ * @param int $code
+ */
+function ajax_response($data, $code = 200) {
+ header('Cache-Control: no-cache, must-revalidate');
+ header('Pragma: no-cache');
+ header('Content-Type: application/json; charset=utf-8');
+ http_response_code($code);
+ echo jsonEncode($data);
+ exit;
+} \ No newline at end of file
diff --git a/localwebsite/functions.php b/localwebsite/functions.php
new file mode 100644
index 0000000..c0c4479
--- /dev/null
+++ b/localwebsite/functions.php
@@ -0,0 +1,255 @@
+<?php
+
+function param($key) {
+ global $RouterInput;
+
+ $val = null;
+
+ if (isset($RouterInput[$key])) {
+ $val = $RouterInput[$key];
+ } else if (isset($_POST[$key])) {
+ $val = $_POST[$key];
+ } else if (isset($_GET[$key])) {
+ $val = $_GET[$key];
+ }
+
+ if (is_array($val)) {
+ $val = implode($val);
+ }
+ return $val;
+}
+
+function str_replace_once(string $needle, string $replace, string $haystack): string {
+ $pos = strpos($haystack, $needle);
+ if ($pos !== false) {
+ $haystack = substr_replace($haystack, $replace, $pos, strlen($needle));
+ }
+ return $haystack;
+}
+
+function htmlescape($s) {
+ if (is_array($s)) {
+ foreach ($s as $k => $v) {
+ $s[$k] = htmlescape($v);
+ }
+ return $s;
+ }
+ return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
+}
+
+function jsonEncode($obj) {
+ return json_encode($obj, JSON_UNESCAPED_UNICODE);
+}
+
+function jsonDecode($json) {
+ return json_decode($json, true);
+}
+
+function startsWith(string $haystack, string $needle): bool {
+ return $needle === "" || strpos($haystack, $needle) === 0;
+}
+
+function endsWith(string $haystack, string $needle): bool {
+ return $needle === "" || substr($haystack, -strlen($needle)) === $needle;
+}
+
+function exectime($format = null) {
+ $time = round(microtime(true) - START_TIME, 4);
+ if (!is_null($format)) {
+ $time = sprintf($format, $time);
+ }
+ return $time;
+}
+
+function stransi($s) {
+ static $colors = [
+ 'black' => 0,
+ 'red' => 1,
+ 'green' => 2,
+ 'yellow' => 3,
+ 'blue' => 4,
+ 'magenta' => 5,
+ 'cyan' => 6,
+ 'white' => 7
+ ];
+ static $valid_styles = ['bold', 'fgbright', 'bgbright'];
+
+ $s = preg_replace_callback('/<(?:e ([a-z, =]+)|\/e)>/', function($match) use ($colors, $valid_styles) {
+ if (empty($match[1])) {
+ return "\033[0m";
+ } else {
+ $codes = [];
+ $args = preg_split('/ +/', $match[1]);
+ $fg = null;
+ $bg = null;
+ $styles = [];
+ foreach ($args as $arg) {
+ list($argname, $argvalue) = explode('=', $arg);
+ $err = false;
+ if ($argname == 'fg' || $argname == 'bg') {
+ if (isset($colors[$argvalue])) {
+ $$argname = $colors[$argvalue];
+ } else {
+ $err = true;
+ }
+ } else if ($argname == 'style') {
+ $argstyles = array_filter(explode(',', $argvalue));
+ foreach ($argstyles as $style) {
+ if (!in_array($style, $valid_styles)) {
+ $err = true;
+ break;
+ }
+ }
+ if (!$err) {
+ foreach ($argstyles as $style) {
+ $styles[$style] = true;
+ }
+ }
+ } else {
+ $err = true;
+ }
+
+ if ($err) {
+ trigger_error(__FUNCTION__.": unrecognized argument {$arg}", E_USER_WARNING);
+ }
+ }
+
+ if (!is_null($fg)) {
+ $codes[] = $fg + (isset($styles['fgbright']) ? 90 : 30);
+ }
+ if (!is_null($bg)) {
+ $codes[] = $bg + (isset($styles['bgbright']) ? 100 : 40);
+ }
+ if (isset($styles['bold'])) {
+ $codes[] = 1;
+ }
+
+ return !empty($codes) ? "\033[".implode(';', $codes)."m" : '';
+ }
+ }, $s);
+ return $s;
+}
+
+function strgen($len = 10): string {
+ $buf = '';
+ for ($i = 0; $i < $len; $i++) {
+ $j = mt_rand(0, 61);
+ if ($j >= 36) {
+ $j += 13;
+ } else if ($j >= 10) {
+ $j += 7;
+ }
+ $buf .= chr(48 + $j);
+ }
+ return $buf;
+}
+
+function setperm($file, $is_dir = null) {
+ global $config;
+
+ // chgrp
+ $gid = filegroup($file);
+ $gname = posix_getgrgid($gid);
+ if (!is_array($gname)) {
+ debugError(__FUNCTION__.": posix_getgrgid() failed on $gid", $gname);
+ } else {
+ $gname = $gname['name'];
+ }
+ if ($gname != $config['group']) {
+ if (!chgrp($file, $config['group'])) {
+ debugError(__FUNCTION__.": chgrp() failed on $file");
+ }
+ }
+
+ // chmod
+ $perms = fileperms($file);
+ $need_perms = is_dir($file) ? $config['dirs_mode'] : $config['files_mode'];
+ if (($perms & $need_perms) !== $need_perms) {
+ if (!chmod($file, $need_perms)) {
+ debugError(__FUNCTION__.": chmod() failed on $file");
+ }
+ }
+}
+
+function redirect($url, $preserve_utm = true, $no_ajax = false) {
+ if (PHP_SAPI != 'cli' && $_SERVER['REQUEST_METHOD'] == 'GET' && $preserve_utm) {
+ $proxy_params = ['utm_source', 'utm_medium', 'utm_content', 'utm_campaign'];
+ $params = [];
+ foreach ($proxy_params as $p) {
+ if (!empty($_GET[$p])) {
+ $params[$p] = (string)$_GET[$p];
+ }
+ }
+ if (!empty($params)) {
+ if (($anchor_pos = strpos($url, '#')) !== false) {
+ $anchor = substr($url, $anchor_pos+1);
+ $url = substr($url, 0, $anchor_pos);
+ }
+ $url .= (strpos($url, '?') === false ? '?' : '&').http_build_query($params);
+ if ($anchor_pos !== false) {
+ $url .= '#'.$anchor;
+ }
+ }
+ }
+
+ header('Location: ' . $url);
+ exit;
+}
+
+function is_xhr_request(): bool {
+ return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
+}
+
+function secondsToTime(int $n): string {
+ $parts = [];
+
+ if ($n >= 86400) {
+ $days = floor($n / 86400);
+ $n %= 86400;
+ $parts[] = "{$days}д";
+ }
+
+ if ($n >= 3600) {
+ $hours = floor($n / 3600);
+ $n %= 3600;
+ $parts[] = "{$hours}ч";
+ }
+
+ if ($n >= 60) {
+ $minutes = floor($n / 60);
+ $n %= 60;
+ $parts[] = "{$minutes}мин";
+ }
+
+ if ($n)
+ $parts[] = "{$n}сек";
+
+ return implode(' ', $parts);
+}
+
+function bytesToUnitsLabel(GMP $b): string {
+ $ks = array('B', 'Kb', 'Mb', 'Gb', 'Tb');
+ foreach ($ks as $i => $k) {
+ if (gmp_cmp($b, gmp_pow(1024, $i + 1)) < 0) {
+ if ($i == 0)
+ return gmp_strval($b) . ' ' . $k;
+
+ $n = gmp_intval(gmp_div_q($b, gmp_pow(1024, $i)));
+ return round($n, 2).' '.$k;
+ }
+ }
+
+ return gmp_strval($b);
+}
+
+$ShutdownFunctions = [];
+
+function append_shutdown_function(callable $f) {
+ global $ShutdownFunctions;
+ $ShutdownFunctions[] = $f;
+}
+
+function prepend_shutdown_function(callable $f) {
+ global $ShutdownFunctions;
+ array_unshift($ShutdownFunctions, $f);
+} \ No newline at end of file
diff --git a/localwebsite/handlers/FakeRequestHandler.php b/localwebsite/handlers/FakeRequestHandler.php
new file mode 100644
index 0000000..09e03b8
--- /dev/null
+++ b/localwebsite/handlers/FakeRequestHandler.php
@@ -0,0 +1,20 @@
+<?php
+
+class FakeRequestHandler extends RequestHandler {
+
+ public function apacheNotFound() {
+ http_response_code(404);
+ $uri = htmlspecialchars($_SERVER['REQUEST_URI']);
+ echo <<<EOF
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+<html><head>
+<title>404 Not Found</title>
+</head><body>
+<h1>Not Found</h1>
+<p>The requested URL {$uri} was not found on this server.</p>
+</body></html>
+EOF;
+ exit;
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/handlers/InverterHandler.php b/localwebsite/handlers/InverterHandler.php
new file mode 100644
index 0000000..7878c4f
--- /dev/null
+++ b/localwebsite/handlers/InverterHandler.php
@@ -0,0 +1,88 @@
+<?php
+
+class InverterHandler extends RequestHandler
+{
+
+ public function __construct()
+ {
+ parent::__construct();
+ $this->tpl->add_static('inverter.js');
+ }
+
+ public function GET_status_page()
+ {
+ global $config;
+ $inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']);
+ $inv->setFormat('json');
+ $status = jsonDecode($inv->exec('get-status'))['data'];
+
+ $this->tpl->set([
+ 'status' => $status,
+ 'html' => $this->renderStatusHtml($status)
+ ]);
+ $this->tpl->set_title('Инвертор');
+ $this->tpl->render_page('inverter_page.twig');
+ }
+
+ public function GET_status_ajax() {
+ global $config;
+ $inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']);
+ $inv->setFormat('json');
+ $status = jsonDecode($inv->exec('get-status'))['data'];
+ ajax_ok(['html' => $this->renderStatusHtml($status)]);
+ }
+
+ protected function renderStatusHtml(array $status)
+ {
+ $power_direction = strtolower($status['battery_power_direction']);
+ $power_direction = preg_replace('/ge$/', 'ging', $power_direction);
+
+ $charging_rate = '';
+ if ($power_direction == 'charging')
+ $charging_rate = sprintf(' @ %s %s',
+ $status['battery_charging_current']['value'],
+ $status['battery_charging_current']['unit']);
+ else if ($power_direction == 'discharging')
+ $charging_rate = sprintf(' @ %s %s',
+ $status['battery_discharging_current']['value'],
+ $status['battery_discharging_current']['unit']);
+
+ $html = sprintf('<b>Battery:</b> %s %s',
+ $status['battery_voltage']['value'],
+ $status['battery_voltage']['unit']);
+ $html .= sprintf(' (%s%s, ',
+ $status['battery_capacity']['value'],
+ $status['battery_capacity']['unit']);
+ $html .= sprintf('%s%s)',
+ $power_direction,
+ $charging_rate);
+
+ $html .= "\n".sprintf('<b>Load:</b> %s %s',
+ $status['ac_output_active_power']['value'],
+ $status['ac_output_active_power']['unit']);
+ $html .= sprintf(' (%s%%)',
+ $status['output_load_percent']['value']);
+
+ if ($status['pv1_input_power']['value'] > 0)
+ $html .= "\n".sprintf('<b>Input power:</b> %s %s',
+ $status['pv1_input_power']['value'],
+ $status['pv1_input_power']['unit']);
+
+ if ($status['grid_voltage']['value'] > 0 or $status['grid_freq']['value'] > 0) {
+ $html .= "\n".sprintf('<b>Generator:</b> %s %s',
+ $status['grid_voltage']['unit'],
+ $status['grid_voltage']['value']);
+ $html .= sprintf(', %s %s',
+ $status['grid_freq']['value'],
+ $status['grid_freq']['unit']);
+ }
+
+ return nl2br($html);
+ }
+
+ public function GET_status_page_update()
+ {
+
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php
new file mode 100644
index 0000000..4f35981
--- /dev/null
+++ b/localwebsite/handlers/MiscHandler.php
@@ -0,0 +1,52 @@
+<?php
+
+class MiscHandler extends RequestHandler
+{
+
+ public function GET_main() {
+ $this->tpl->set_title('Главная');
+ $this->tpl->render_page('index.twig');
+ }
+
+ public function GET_phpinfo() {
+ phpinfo();
+ exit;
+ }
+
+ public function GET_sensors_page() {
+ global $config;
+
+ $clients = [];
+ foreach ($config['si7021d_servers'] as $key => $params) {
+ $cl = new Si7021dClient(...$params);
+ $clients[$key] = $cl;
+
+ $cl->readSensor();
+ }
+
+ $this->tpl->set(['sensors' => $clients]);
+ $this->tpl->set_title('Датчики');
+ $this->tpl->render_page('sensors.twig');
+ }
+
+ public function GET_pump_page() {
+ global $config;
+
+ list($set) = $this->input('set');
+ $client = new GPIORelaydClient($config['pump_host'], $config['pump_port']);
+
+ if ($set == GPIORelaydClient::STATUS_ON || $set == GPIORelaydClient::STATUS_OFF) {
+ $client->setStatus($set);
+ redirect('/pump/');
+ }
+
+ $status = $client->getStatus();
+
+ $this->tpl->set([
+ 'status' => $status
+ ]);
+ $this->tpl->set_title('Насос');
+ $this->tpl->render_page('pump.twig');
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php
new file mode 100644
index 0000000..88fe463
--- /dev/null
+++ b/localwebsite/handlers/ModemHandler.php
@@ -0,0 +1,192 @@
+<?php
+
+class ModemHandler extends RequestHandler
+{
+
+ public function __construct()
+ {
+ parent::__construct();
+ $this->tpl->add_static('modem.js');
+ }
+
+ public function GET_status_page() {
+ global $config;
+
+ $this->tpl->set([
+ 'modems' => $config['modems'],
+ 'js_modems' => array_keys($config['modems']),
+ ]);
+
+ $this->tpl->set_title('Состояние модемов');
+ $this->tpl->render_page('modem_status_page.twig');
+ }
+
+ public function GET_status_get_ajax() {
+ global $config;
+ list($id) = $this->input('id');
+ if (!isset($config['modems'][$id]))
+ ajax_error('invalid modem id: '.$id);
+
+ $modem_data = self::getModemData(
+ $config['modems'][$id]['ip'],
+ $config['modems'][$id]['legacy_token_auth']);
+
+ ajax_ok([
+ 'html' => $this->tpl->render('modem_data.twig', [
+ 'loading' => false,
+ 'modem_data' => $modem_data
+ ])
+ ]);
+ }
+
+
+ public function GET_routing_smallhome_page() {
+ global $config;
+
+ list($error) = $this->input('error');
+ $upstream = self::getCurrentSmallHomeUpstream();
+
+ $current_upstream = [
+ 'key' => $upstream,
+ 'label' => $config['modems'][$upstream]['label']
+ ];
+
+ $this->tpl->set([
+ 'error' => $error,
+ 'current' => $current_upstream,
+ 'modems' => $config['modems'],
+ ]);
+ $this->tpl->set_title('Маршрутизация');
+ $this->tpl->render_page('routing_page.twig');
+ }
+
+ public function GET_routing_smallhome_switch() {
+ global $config;
+ list($new_upstream) = $this->input('upstream');
+ if (!isset($config['modems'][$new_upstream]))
+ redirect('/routing/?error='.urlencode('invalid upstream'));
+
+ $current_upstream = self::getCurrentSmallHomeUpstream();
+ if ($current_upstream != $new_upstream) {
+ if ($current_upstream != $config['routing_default'])
+ MyOpenWrtUtils::ipsetDel($current_upstream, $config['routing_smallhome_ip']);
+ if ($new_upstream != $config['routing_default'])
+ MyOpenWrtUtils::ipsetAdd($new_upstream, $config['routing_smallhome_ip']);
+ }
+
+ redirect('/routing/');
+ }
+
+ public function GET_routing_ipsets_page() {
+ list($error) = $this->input('error');
+
+ $ip_sets = MyOpenWrtUtils::ipsetListAll();
+ $this->tpl->set([
+ 'sets' => $ip_sets,
+ 'error' => $error
+ ]);
+ $this->tpl->set_title('Маршрутизация: IP sets');
+ $this->tpl->render_page('routing_ipsets_page.twig');
+ }
+
+ public function GET_routing_ipsets_del() {
+ list($set, $ip) = $this->input('set, ip');
+ self::validateIpsetsInput($set, $ip);
+
+ $output = MyOpenWrtUtils::ipsetDel($set, $ip);
+
+ $url = '/routing/ipsets/';
+ if ($output != '')
+ $url .= '?error='.urlencode($output);
+ redirect($url);
+ }
+
+ public function POST_routing_ipsets_add() {
+ list($set, $ip) = $this->input('set, ip');
+ self::validateIpsetsInput($set, $ip);
+
+ $output = MyOpenWrtUtils::ipsetAdd($set, $ip);
+
+ $url = '/routing/ipsets/';
+ if ($output != '')
+ $url .= '?error='.urlencode($output);
+ redirect($url);
+ }
+
+ public function GET_routing_dhcp_page() {
+ $leases = MyOpenWrtUtils::getDHCPLeases();
+ $this->tpl->set([
+ 'leases' => $leases
+ ]);
+ $this->tpl->set_title('Маршрутизация: DHCP');
+ $this->tpl->render_page('routing_dhcp_page.twig');
+ }
+
+ public function GET_sms_page() {
+ global $config;
+
+ list($selected) = $this->input('modem');
+ if (!$selected)
+ $selected = array_key_first($config['modems']);
+
+ $cfg = $config['modems'][$selected];
+ $e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']);
+ $messages = $e3372->getSMSList();
+
+ $this->tpl->set([
+ 'modems_list' => array_keys($config['modems']),
+ 'modems' => $config['modems'],
+ 'selected_modem' => $selected,
+ 'messages' => $messages
+ ]);
+ $this->tpl->set_title('Модемы: SMS-сообщения');
+ $this->tpl->render_page('sms_page.twig');
+ }
+
+ protected static function getModemData(string $ip, bool $need_auth = true): array {
+ $modem = new E3372($ip, $need_auth);
+ $signal = $modem->getDeviceSignal();
+ $status = $modem->getMonitoringStatus();
+ $traffic = $modem->getTrafficStats();
+ return [
+ 'type' => e3372::getNetworkTypeLabel($status['CurrentNetworkType']),
+ 'level' => $status['SignalIcon'] ?? 0,
+ 'rssi' => $signal['rssi'],
+ 'sinr' => $signal['sinr'],
+ 'connected_time' => secondsToTime($traffic['CurrentConnectTime']),
+ 'downloaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentDownload'])),
+ 'uploaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentUpload'])),
+ ];
+ }
+
+ protected static function getCurrentSmallHomeUpstream() {
+ global $config;
+
+ $upstream = null;
+ $ip_sets = MyOpenWrtUtils::ipsetListAll();
+ foreach ($ip_sets as $set => $ips) {
+ if (in_array($config['routing_smallhome_ip'], $ips)) {
+ $upstream = $set;
+ break;
+ }
+ }
+ if (is_null($upstream))
+ $upstream = $config['routing_default'];
+
+ return $upstream;
+ }
+
+ protected static function validateIpsetsInput($set, $ip) {
+ global $config;
+
+ if (!isset($config['modems'][$set]))
+ redirect('/routing/ipsets/?error='.urlencode('invalid set: '.$set));
+
+ if (($slashpos = strpos($ip, '/')) !== false)
+ $ip = substr($ip, 0, $slashpos);
+
+ if (!filter_var($ip, FILTER_VALIDATE_IP))
+ redirect('/routing/ipsets/?error='.urlencode('invalid ip/network: '.$ip));
+ }
+
+} \ No newline at end of file
diff --git a/localwebsite/handlers/RequestHandler.php b/localwebsite/handlers/RequestHandler.php
new file mode 100644
index 0000000..2fffdc0
--- /dev/null
+++ b/localwebsite/handlers/RequestHandler.php
@@ -0,0 +1,41 @@
+<?php
+
+class RequestHandler extends request_handler {
+
+ /** @var web_tpl*/
+ protected $tpl;
+
+ public function __construct() {
+ global $__tpl;
+ $__tpl = new web_tpl();
+ $this->tpl = $__tpl;
+
+ $this->tpl->add_static('bootstrap.min.css');
+ $this->tpl->add_static('bootstrap.min.js');
+ $this->tpl->add_static('polyfills.js');
+ $this->tpl->add_static('app.js');
+ $this->tpl->add_static('app.css');
+ }
+
+ public function dispatch(string $act) {
+ global $config;
+ $this->tpl->set_global([
+ '__dev' => $config['is_dev'],
+ ]);
+ return parent::dispatch($act);
+ }
+
+ protected function method_not_found(string $method, string $act)
+ {
+ global $config;
+
+ if ($act != '404' && $config['is_dev'])
+ debugError(get_called_class() . ": act {$method}_{$act} not found.");
+
+ if (!is_xhr_request())
+ $this->tpl->render_not_found();
+ else
+ ajax_error('unknown act "'.$act.'"', 404);
+
+ }
+} \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/app.css b/localwebsite/htdocs/assets/app.css
new file mode 100644
index 0000000..cab674e
--- /dev/null
+++ b/localwebsite/htdocs/assets/app.css
@@ -0,0 +1,153 @@
+.signal_level {
+ display: inline-block;
+}
+.signal_level > div {
+ width: 10px;
+ height: 10px;
+ background: #ddd;
+ border-radius: 50%;
+ float: left;
+ margin-right: 2px;
+}
+.signal_level > div.yes {
+ background-color: #5acc61;
+}
+
+
+/** spinner.twig **/
+
+.sk-fading-circle {
+ margin-top: 10px;
+ width: 20px;
+ height: 20px;
+ position: relative;
+}
+
+.sk-fading-circle .sk-circle {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+
+.sk-fading-circle .sk-circle:before {
+ content: '';
+ display: block;
+ margin: 0 auto;
+ width: 15%;
+ height: 15%;
+ background-color: #555;
+ border-radius: 100%;
+ -webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;
+ animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;
+}
+.sk-fading-circle .sk-circle2 {
+ -webkit-transform: rotate(30deg);
+ -ms-transform: rotate(30deg);
+ transform: rotate(30deg);
+}
+.sk-fading-circle .sk-circle3 {
+ -webkit-transform: rotate(60deg);
+ -ms-transform: rotate(60deg);
+ transform: rotate(60deg);
+}
+.sk-fading-circle .sk-circle4 {
+ -webkit-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+.sk-fading-circle .sk-circle5 {
+ -webkit-transform: rotate(120deg);
+ -ms-transform: rotate(120deg);
+ transform: rotate(120deg);
+}
+.sk-fading-circle .sk-circle6 {
+ -webkit-transform: rotate(150deg);
+ -ms-transform: rotate(150deg);
+ transform: rotate(150deg);
+}
+.sk-fading-circle .sk-circle7 {
+ -webkit-transform: rotate(180deg);
+ -ms-transform: rotate(180deg);
+ transform: rotate(180deg);
+}
+.sk-fading-circle .sk-circle8 {
+ -webkit-transform: rotate(210deg);
+ -ms-transform: rotate(210deg);
+ transform: rotate(210deg);
+}
+.sk-fading-circle .sk-circle9 {
+ -webkit-transform: rotate(240deg);
+ -ms-transform: rotate(240deg);
+ transform: rotate(240deg);
+}
+.sk-fading-circle .sk-circle10 {
+ -webkit-transform: rotate(270deg);
+ -ms-transform: rotate(270deg);
+ transform: rotate(270deg);
+}
+.sk-fading-circle .sk-circle11 {
+ -webkit-transform: rotate(300deg);
+ -ms-transform: rotate(300deg);
+ transform: rotate(300deg);
+}
+.sk-fading-circle .sk-circle12 {
+ -webkit-transform: rotate(330deg);
+ -ms-transform: rotate(330deg);
+ transform: rotate(330deg);
+}
+.sk-fading-circle .sk-circle2:before {
+ -webkit-animation-delay: -1.1s;
+ animation-delay: -1.1s;
+}
+.sk-fading-circle .sk-circle3:before {
+ -webkit-animation-delay: -1s;
+ animation-delay: -1s;
+}
+.sk-fading-circle .sk-circle4:before {
+ -webkit-animation-delay: -0.9s;
+ animation-delay: -0.9s;
+}
+.sk-fading-circle .sk-circle5:before {
+ -webkit-animation-delay: -0.8s;
+ animation-delay: -0.8s;
+}
+.sk-fading-circle .sk-circle6:before {
+ -webkit-animation-delay: -0.7s;
+ animation-delay: -0.7s;
+}
+.sk-fading-circle .sk-circle7:before {
+ -webkit-animation-delay: -0.6s;
+ animation-delay: -0.6s;
+}
+.sk-fading-circle .sk-circle8:before {
+ -webkit-animation-delay: -0.5s;
+ animation-delay: -0.5s;
+}
+.sk-fading-circle .sk-circle9:before {
+ -webkit-animation-delay: -0.4s;
+ animation-delay: -0.4s;
+}
+.sk-fading-circle .sk-circle10:before {
+ -webkit-animation-delay: -0.3s;
+ animation-delay: -0.3s;
+}
+.sk-fading-circle .sk-circle11:before {
+ -webkit-animation-delay: -0.2s;
+ animation-delay: -0.2s;
+}
+.sk-fading-circle .sk-circle12:before {
+ -webkit-animation-delay: -0.1s;
+ animation-delay: -0.1s;
+}
+
+@-webkit-keyframes sk-circleFadeDelay {
+ 0%, 39%, 100% { opacity: 0; }
+ 40% { opacity: 1; }
+}
+
+@keyframes sk-circleFadeDelay {
+ 0%, 39%, 100% { opacity: 0; }
+ 40% { opacity: 1; }
+} \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/app.js b/localwebsite/htdocs/assets/app.js
new file mode 100644
index 0000000..da5052d
--- /dev/null
+++ b/localwebsite/htdocs/assets/app.js
@@ -0,0 +1,44 @@
+var ajax = {
+ get: function(url, data) {
+ if (typeof data == 'object') {
+ var index = 0;
+ for (var key in data) {
+ var val = data[key];
+ url += index === 0 && url.indexOf('?') === -1 ? '?' : '&';
+ url += encodeURIComponent(key) + '=' + encodeURIComponent(val);
+ }
+ }
+ return this.raw(url);
+ },
+
+ post: function(url, body) {
+ var opts = {
+ method: 'POST'
+ };
+ if (body)
+ opts.body = body;
+ return this.raw(url, opts);
+ },
+
+ raw: function(url, options) {
+ if (!options)
+ options = {}
+
+ return fetch(url, Object.assign({
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ }
+ }, options))
+ .then(resp => {
+ return resp.json()
+ })
+ }
+};
+
+function extend(a, b) {
+ return Object.assign(a, b);
+}
+
+function ge(id) {
+ return document.getElementById(id);
+} \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/bootstrap.min.css b/localwebsite/htdocs/assets/bootstrap.min.css
new file mode 100644
index 0000000..edfbbb0
--- /dev/null
+++ b/localwebsite/htdocs/assets/bootstrap.min.css
@@ -0,0 +1,7 @@
+@charset "UTF-8";/*!
+ * Bootstrap v5.0.2 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors
+ * Copyright 2011-2021 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + (.5rem + 2px));padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + (1rem + 2px));padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + (.75rem + 2px))}textarea.form-control-sm{min-height:calc(1.5em + (.5rem + 2px))}textarea.form-control-lg{min-height:calc(1.5em + (1rem + 2px))}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1050;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/bootstrap.min.js b/localwebsite/htdocs/assets/bootstrap.min.js
new file mode 100644
index 0000000..aed031f
--- /dev/null
+++ b/localwebsite/htdocs/assets/bootstrap.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v5.0.2 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){if(t&&t.__esModule)return t;var e=Object.create(null);return t&&Object.keys(t).forEach((function(s){if("default"!==s){var i=Object.getOwnPropertyDescriptor(t,s);Object.defineProperty(e,s,i.get?i:{enumerable:!0,get:function(){return t[s]}})}})),e.default=t,Object.freeze(e)}var s=e(t);const i={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const s=[];let i=t.parentNode;for(;i&&i.nodeType===Node.ELEMENT_NODE&&3!==i.nodeType;)i.matches(e)&&s.push(i),i=i.parentNode;return s},prev(t,e){let s=t.previousElementSibling;for(;s;){if(s.matches(e))return[s];s=s.previousElementSibling}return[]},next(t,e){let s=t.nextElementSibling;for(;s;){if(s.matches(e))return[s];s=s.nextElementSibling}return[]}},n=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},o=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let s=t.getAttribute("href");if(!s||!s.includes("#")&&!s.startsWith("."))return null;s.includes("#")&&!s.startsWith("#")&&(s="#"+s.split("#")[1]),e=s&&"#"!==s?s.trim():null}return e},r=t=>{const e=o(t);return e&&document.querySelector(e)?e:null},a=t=>{const e=o(t);return e?document.querySelector(e):null},l=t=>{t.dispatchEvent(new Event("transitionend"))},c=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),h=t=>c(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?i.findOne(t):null,d=(t,e,s)=>{Object.keys(s).forEach(i=>{const n=s[i],o=e[i],r=o&&c(o)?"element":null==(a=o)?""+a:{}.toString.call(a).match(/\s([a-z]+)/i)[1].toLowerCase();var a;if(!new RegExp(n).test(r))throw new TypeError(`${t.toUpperCase()}: Option "${i}" provided type "${r}" but expected type "${n}".`)})},u=t=>!(!c(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),g=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),p=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?p(t.parentNode):null},f=()=>{},m=t=>t.offsetHeight,_=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},b=[],v=()=>"rtl"===document.documentElement.dir,y=t=>{var e;e=()=>{const e=_();if(e){const s=t.NAME,i=e.fn[s];e.fn[s]=t.jQueryInterface,e.fn[s].Constructor=t,e.fn[s].noConflict=()=>(e.fn[s]=i,t.jQueryInterface)}},"loading"===document.readyState?(b.length||document.addEventListener("DOMContentLoaded",()=>{b.forEach(t=>t())}),b.push(e)):e()},w=t=>{"function"==typeof t&&t()},E=(t,e,s=!0)=>{if(!s)return void w(t);const i=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:s}=window.getComputedStyle(t);const i=Number.parseFloat(e),n=Number.parseFloat(s);return i||n?(e=e.split(",")[0],s=s.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(s))):0})(e)+5;let n=!1;const o=({target:s})=>{s===e&&(n=!0,e.removeEventListener("transitionend",o),w(t))};e.addEventListener("transitionend",o),setTimeout(()=>{n||l(e)},i)},A=(t,e,s,i)=>{let n=t.indexOf(e);if(-1===n)return t[!s&&i?t.length-1:0];const o=t.length;return n+=s?1:-1,i&&(n=(n+o)%o),t[Math.max(0,Math.min(n,o-1))]},T=/[^.]*(?=\..*)\.|.*/,C=/\..*/,k=/::\d+$/,L={};let O=1;const D={mouseenter:"mouseover",mouseleave:"mouseout"},I=/^(mouseenter|mouseleave)/i,N=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function S(t,e){return e&&`${e}::${O++}`||t.uidEvent||O++}function x(t){const e=S(t);return t.uidEvent=e,L[e]=L[e]||{},L[e]}function M(t,e,s=null){const i=Object.keys(t);for(let n=0,o=i.length;n<o;n++){const o=t[i[n]];if(o.originalHandler===e&&o.delegationSelector===s)return o}return null}function P(t,e,s){const i="string"==typeof e,n=i?s:e;let o=R(t);return N.has(o)||(o=t),[i,n,o]}function j(t,e,s,i,n){if("string"!=typeof e||!t)return;if(s||(s=i,i=null),I.test(e)){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};i?i=t(i):s=t(s)}const[o,r,a]=P(e,s,i),l=x(t),c=l[a]||(l[a]={}),h=M(c,r,o?s:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=S(r,e.replace(T,"")),u=o?function(t,e,s){return function i(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return n.delegateTarget=r,i.oneOff&&B.off(t,n.type,e,s),s.apply(r,[n]);return null}}(t,s,i):function(t,e){return function s(i){return i.delegateTarget=t,s.oneOff&&B.off(t,i.type,e),e.apply(t,[i])}}(t,s);u.delegationSelector=o?s:null,u.originalHandler=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function H(t,e,s,i,n){const o=M(e[s],i,n);o&&(t.removeEventListener(s,o,Boolean(n)),delete e[s][o.uidEvent])}function R(t){return t=t.replace(C,""),D[t]||t}const B={on(t,e,s,i){j(t,e,s,i,!1)},one(t,e,s,i){j(t,e,s,i,!0)},off(t,e,s,i){if("string"!=typeof e||!t)return;const[n,o,r]=P(e,s,i),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void H(t,l,r,o,n?s:null)}c&&Object.keys(l).forEach(s=>{!function(t,e,s,i){const n=e[s]||{};Object.keys(n).forEach(o=>{if(o.includes(i)){const i=n[o];H(t,e,s,i.originalHandler,i.delegationSelector)}})}(t,l,s,e.slice(1))});const h=l[r]||{};Object.keys(h).forEach(s=>{const i=s.replace(k,"");if(!a||e.includes(i)){const e=h[s];H(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,s){if("string"!=typeof e||!t)return null;const i=_(),n=R(e),o=e!==n,r=N.has(n);let a,l=!0,c=!0,h=!1,d=null;return o&&i&&(a=i.Event(e,s),i(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(n,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==s&&Object.keys(s).forEach(t=>{Object.defineProperty(d,t,{get:()=>s[t]})}),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},$=new Map;var W={set(t,e,s){$.has(t)||$.set(t,new Map);const i=$.get(t);i.has(e)||0===i.size?i.set(e,s):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(i.keys())[0]}.`)},get:(t,e)=>$.has(t)&&$.get(t).get(e)||null,remove(t,e){if(!$.has(t))return;const s=$.get(t);s.delete(e),0===s.size&&$.delete(t)}};class q{constructor(t){(t=h(t))&&(this._element=t,W.set(this._element,this.constructor.DATA_KEY,this))}dispose(){W.remove(this._element,this.constructor.DATA_KEY),B.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach(t=>{this[t]=null})}_queueCallback(t,e,s=!0){E(t,e,s)}static getInstance(t){return W.get(t,this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.0.2"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return"bs."+this.NAME}static get EVENT_KEY(){return"."+this.DATA_KEY}}class z extends q{static get NAME(){return"alert"}close(t){const e=t?this._getRootElement(t):this._element,s=this._triggerCloseEvent(e);null===s||s.defaultPrevented||this._removeElement(e)}_getRootElement(t){return a(t)||t.closest(".alert")}_triggerCloseEvent(t){return B.trigger(t,"close.bs.alert")}_removeElement(t){t.classList.remove("show");const e=t.classList.contains("fade");this._queueCallback(()=>this._destroyElement(t),t,e)}_destroyElement(t){t.remove(),B.trigger(t,"closed.bs.alert")}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"close"===t&&e[t](this)}))}static handleDismiss(t){return function(e){e&&e.preventDefault(),t.close(this)}}}B.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',z.handleDismiss(new z)),y(z);class F extends q{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=F.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function U(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function K(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}B.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');F.getOrCreateInstance(e).toggle()}),y(F);const V={setDataAttribute(t,e,s){t.setAttribute("data-bs-"+K(e),s)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+K(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(s=>{let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=U(t.dataset[s])}),e},getDataAttribute:(t,e)=>U(t.getAttribute("data-bs-"+K(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},Q={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},X={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Y="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z};class et extends q{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=i.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return Q}static get NAME(){return"carousel"}next(){this._slide(Y)}nextWhenVisible(){!document.hidden&&u(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),i.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(l(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=i.findOne(".active.carousel-item",this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void B.one(this._element,"slid.bs.carousel",()=>this.to(t));if(e===t)return this.pause(),void this.cycle();const s=t>e?Y:G;this._slide(s,this._items[t])}_getConfig(t){return t={...Q,...V.getDataAttributes(this._element),..."object"==typeof t?t:{}},d("carousel",t,X),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&B.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(B.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),B.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},e=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},s=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};i.find(".carousel-item img",this._element).forEach(t=>{B.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(B.on(this._element,"pointerdown.bs.carousel",e=>t(e)),B.on(this._element,"pointerup.bs.carousel",t=>s(t)),this._element.classList.add("pointer-event")):(B.on(this._element,"touchstart.bs.carousel",e=>t(e)),B.on(this._element,"touchmove.bs.carousel",t=>e(t)),B.on(this._element,"touchend.bs.carousel",t=>s(t)))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?i.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const s=t===Y;return A(this._items,e,s,this._config.wrap)}_triggerSlideEvent(t,e){const s=this._getItemIndex(t),n=this._getItemIndex(i.findOne(".active.carousel-item",this._element));return B.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:s})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=i.findOne(".active",this._indicatorsElement);e.classList.remove("active"),e.removeAttribute("aria-current");const s=i.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e<s.length;e++)if(Number.parseInt(s[e].getAttribute("data-bs-slide-to"),10)===this._getItemIndex(t)){s[e].classList.add("active"),s[e].setAttribute("aria-current","true");break}}}_updateInterval(){const t=this._activeElement||i.findOne(".active.carousel-item",this._element);if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}_slide(t,e){const s=this._directionToOrder(t),n=i.findOne(".active.carousel-item",this._element),o=this._getItemIndex(n),r=e||this._getItemByOrder(s,n),a=this._getItemIndex(r),l=Boolean(this._interval),c=s===Y,h=c?"carousel-item-start":"carousel-item-end",d=c?"carousel-item-next":"carousel-item-prev",u=this._orderToDirection(s);if(r&&r.classList.contains("active"))return void(this._isSliding=!1);if(this._isSliding)return;if(this._triggerSlideEvent(r,u).defaultPrevented)return;if(!n||!r)return;this._isSliding=!0,l&&this.pause(),this._setActiveIndicatorElement(r),this._activeElement=r;const g=()=>{B.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:u,from:o,to:a})};if(this._element.classList.contains("slide")){r.classList.add(d),m(r),n.classList.add(h),r.classList.add(h);const t=()=>{r.classList.remove(h,d),r.classList.add("active"),n.classList.remove("active",d,h),this._isSliding=!1,setTimeout(g,0)};this._queueCallback(t,n,!0)}else n.classList.remove("active"),r.classList.add("active"),this._isSliding=!1,g();l&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?v()?t===Z?G:Y:t===Z?Y:G:t}_orderToDirection(t){return[Y,G].includes(t)?v()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const s=et.getOrCreateInstance(t,e);let{_config:i}=s;"object"==typeof e&&(i={...i,...e});const n="string"==typeof e?e:i.slide;if("number"==typeof e)s.to(e);else if("string"==typeof n){if(void 0===s[n])throw new TypeError(`No method named "${n}"`);s[n]()}else i.interval&&i.ride&&(s.pause(),s.cycle())}static jQueryInterface(t){return this.each((function(){et.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=a(this);if(!e||!e.classList.contains("carousel"))return;const s={...V.getDataAttributes(e),...V.getDataAttributes(this)},i=this.getAttribute("data-bs-slide-to");i&&(s.interval=!1),et.carouselInterface(e,s),i&&et.getInstance(e).to(i),t.preventDefault()}}B.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",et.dataApiClickHandler),B.on(window,"load.bs.carousel.data-api",()=>{const t=i.find('[data-bs-ride="carousel"]');for(let e=0,s=t.length;e<s;e++)et.carouselInterface(t[e],et.getInstance(t[e]))}),y(et);const st={toggle:!0,parent:""},it={toggle:"boolean",parent:"(string|element)"};class nt extends q{constructor(t,e){super(t),this._isTransitioning=!1,this._config=this._getConfig(e),this._triggerArray=i.find(`[data-bs-toggle="collapse"][href="#${this._element.id}"],[data-bs-toggle="collapse"][data-bs-target="#${this._element.id}"]`);const s=i.find('[data-bs-toggle="collapse"]');for(let t=0,e=s.length;t<e;t++){const e=s[t],n=r(e),o=i.find(n).filter(t=>t===this._element);null!==n&&o.length&&(this._selector=n,this._triggerArray.push(e))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return st}static get NAME(){return"collapse"}toggle(){this._element.classList.contains("show")?this.hide():this.show()}show(){if(this._isTransitioning||this._element.classList.contains("show"))return;let t,e;this._parent&&(t=i.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains("collapse")),0===t.length&&(t=null));const s=i.findOne(this._selector);if(t){const i=t.find(t=>s!==t);if(e=i?nt.getInstance(i):null,e&&e._isTransitioning)return}if(B.trigger(this._element,"show.bs.collapse").defaultPrevented)return;t&&t.forEach(t=>{s!==t&&nt.collapseInterface(t,"hide"),e||W.set(t,"bs.collapse",null)});const n=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[n]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove("collapsed"),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);const o="scroll"+(n[0].toUpperCase()+n.slice(1));this._queueCallback(()=>{this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[n]="",this.setTransitioning(!1),B.trigger(this._element,"shown.bs.collapse")},this._element,!0),this._element.style[n]=this._element[o]+"px"}hide(){if(this._isTransitioning||!this._element.classList.contains("show"))return;if(B.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",m(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;if(e>0)for(let t=0;t<e;t++){const e=this._triggerArray[t],s=a(e);s&&!s.classList.contains("show")&&(e.classList.add("collapsed"),e.setAttribute("aria-expanded",!1))}this.setTransitioning(!0),this._element.style[t]="",this._queueCallback(()=>{this.setTransitioning(!1),this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),B.trigger(this._element,"hidden.bs.collapse")},this._element,!0)}setTransitioning(t){this._isTransitioning=t}_getConfig(t){return(t={...st,...t}).toggle=Boolean(t.toggle),d("collapse",t,it),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let{parent:t}=this._config;t=h(t);const e=`[data-bs-toggle="collapse"][data-bs-parent="${t}"]`;return i.find(e,t).forEach(t=>{const e=a(t);this._addAriaAndCollapsedClass(e,[t])}),t}_addAriaAndCollapsedClass(t,e){if(!t||!e.length)return;const s=t.classList.contains("show");e.forEach(t=>{s?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",s)})}static collapseInterface(t,e){let s=nt.getInstance(t);const i={...st,...V.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!s&&i.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(i.toggle=!1),s||(s=new nt(t,i)),"string"==typeof e){if(void 0===s[e])throw new TypeError(`No method named "${e}"`);s[e]()}}static jQueryInterface(t){return this.each((function(){nt.collapseInterface(this,t)}))}}B.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=V.getDataAttributes(this),s=r(this);i.find(s).forEach(t=>{const s=nt.getInstance(t);let i;s?(null===s._parent&&"string"==typeof e.parent&&(s._config.parent=e.parent,s._parent=s._getParent()),i="toggle"):i=e,nt.collapseInterface(t,i)})})),y(nt);const ot=new RegExp("ArrowUp|ArrowDown|Escape"),rt=v()?"top-end":"top-start",at=v()?"top-start":"top-end",lt=v()?"bottom-end":"bottom-start",ct=v()?"bottom-start":"bottom-end",ht=v()?"left-start":"right-start",dt=v()?"right-start":"left-start",ut={offset:[0,2],boundary:"clippingParents",reference:"toggle",display:"dynamic",popperConfig:null,autoClose:!0},gt={offset:"(array|string|function)",boundary:"(string|element)",reference:"(string|element|object)",display:"string",popperConfig:"(null|object|function)",autoClose:"(boolean|string)"};class pt extends q{constructor(t,e){super(t),this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}static get Default(){return ut}static get DefaultType(){return gt}static get NAME(){return"dropdown"}toggle(){g(this._element)||(this._element.classList.contains("show")?this.hide():this.show())}show(){if(g(this._element)||this._menu.classList.contains("show"))return;const t=pt.getParentFromElement(this._element),e={relatedTarget:this._element};if(!B.trigger(this._element,"show.bs.dropdown",e).defaultPrevented){if(this._inNavbar)V.setDataAttribute(this._menu,"popper","none");else{if(void 0===s)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:c(this._config.reference)?e=h(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find(t=>"applyStyles"===t.name&&!1===t.enabled);this._popper=s.createPopper(e,this._menu,i),n&&V.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!t.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>B.on(t,"mouseover",f)),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),B.trigger(this._element,"shown.bs.dropdown",e)}}hide(){if(g(this._element)||!this._menu.classList.contains("show"))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){B.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_completeHide(t){B.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>B.off(t,"mouseover",f)),this._popper&&this._popper.destroy(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),V.removeDataAttribute(this._menu,"popper"),B.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...V.getDataAttributes(this._element),...t},d("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!c(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_getMenuElement(){return i.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ht;if(t.classList.contains("dropstart"))return dt;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?at:rt:e?ct:lt}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const s=i.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(u);s.length&&A(s,e,"ArrowDown"===t,!s.includes(e)).focus()}static dropdownInterface(t,e){const s=pt.getOrCreateInstance(t,e);if("string"==typeof e){if(void 0===s[e])throw new TypeError(`No method named "${e}"`);s[e]()}}static jQueryInterface(t){return this.each((function(){pt.dropdownInterface(this,t)}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=i.find('[data-bs-toggle="dropdown"]');for(let s=0,i=e.length;s<i;s++){const i=pt.getInstance(e[s]);if(!i||!1===i._config.autoClose)continue;if(!i._element.classList.contains("show"))continue;const n={relatedTarget:i._element};if(t){const e=t.composedPath(),s=e.includes(i._menu);if(e.includes(i._element)||"inside"===i._config.autoClose&&!s||"outside"===i._config.autoClose&&s)continue;if(i._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;"click"===t.type&&(n.clickEvent=t)}i._completeHide(n)}}static getParentFromElement(t){return a(t)||t.parentNode}static dataApiKeydownHandler(t){if(/input|textarea/i.test(t.target.tagName)?"Space"===t.key||"Escape"!==t.key&&("ArrowDown"!==t.key&&"ArrowUp"!==t.key||t.target.closest(".dropdown-menu")):!ot.test(t.key))return;const e=this.classList.contains("show");if(!e&&"Escape"===t.key)return;if(t.preventDefault(),t.stopPropagation(),g(this))return;const s=()=>this.matches('[data-bs-toggle="dropdown"]')?this:i.prev(this,'[data-bs-toggle="dropdown"]')[0];return"Escape"===t.key?(s().focus(),void pt.clearMenus()):"ArrowUp"===t.key||"ArrowDown"===t.key?(e||s().click(),void pt.getInstance(s())._selectMenuItem(t)):void(e&&"Space"!==t.key||pt.clearMenus())}}B.on(document,"keydown.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',pt.dataApiKeydownHandler),B.on(document,"keydown.bs.dropdown.data-api",".dropdown-menu",pt.dataApiKeydownHandler),B.on(document,"click.bs.dropdown.data-api",pt.clearMenus),B.on(document,"keyup.bs.dropdown.data-api",pt.clearMenus),B.on(document,"click.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',(function(t){t.preventDefault(),pt.dropdownInterface(this)})),y(pt);class ft{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,"paddingRight",e=>e+t),this._setElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight",e=>e+t),this._setElementAttributes(".sticky-top","marginRight",e=>e-t)}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,s){const i=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+i)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t)[e];t.style[e]=s(Number.parseFloat(n))+"px"})}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight")}_saveInitialAttribute(t,e){const s=t.style[e];s&&V.setDataAttribute(t,e,s)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const s=V.getDataAttribute(t,e);void 0===s?t.style.removeProperty(e):(V.removeDataAttribute(t,e),t.style[e]=s)})}_applyManipulationCallback(t,e){c(t)?e(t):i.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const mt={isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},_t={isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"};class bt{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&m(this._getElement()),this._getElement().classList.add("show"),this._emulateAnimation(()=>{w(t)})):w(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove("show"),this._emulateAnimation(()=>{this.dispose(),w(t)})):w(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className="modal-backdrop",this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...mt,..."object"==typeof t?t:{}}).rootElement=h(t.rootElement),d("backdrop",t,_t),t}_append(){this._isAppended||(this._config.rootElement.appendChild(this._getElement()),B.on(this._getElement(),"mousedown.bs.backdrop",()=>{w(this._config.clickCallback)}),this._isAppended=!0)}dispose(){this._isAppended&&(B.off(this._element,"mousedown.bs.backdrop"),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){E(t,this._getElement(),this._config.isAnimated)}}const vt={backdrop:!0,keyboard:!0,focus:!0},yt={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"};class wt extends q{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=i.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new ft}static get Default(){return vt}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||B.trigger(this._element,"show.bs.modal",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add("modal-open"),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),B.on(this._element,"click.dismiss.bs.modal",'[data-bs-dismiss="modal"]',t=>this.hide(t)),B.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{B.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){if(t&&["A","AREA"].includes(t.target.tagName)&&t.preventDefault(),!this._isShown||this._isTransitioning)return;if(B.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const e=this._isAnimated();e&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),B.off(document,"focusin.bs.modal"),this._element.classList.remove("show"),B.off(this._element,"click.dismiss.bs.modal"),B.off(this._dialog,"mousedown.dismiss.bs.modal"),this._queueCallback(()=>this._hideModal(),this._element,e)}dispose(){[window,this._dialog].forEach(t=>B.off(t,".bs.modal")),this._backdrop.dispose(),super.dispose(),B.off(document,"focusin.bs.modal")}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_getConfig(t){return t={...vt,...V.getDataAttributes(this._element),..."object"==typeof t?t:{}},d("modal",t,yt),t}_showElement(t){const e=this._isAnimated(),s=i.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,s&&(s.scrollTop=0),e&&m(this._element),this._element.classList.add("show"),this._config.focus&&this._enforceFocus(),this._queueCallback(()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,B.trigger(this._element,"shown.bs.modal",{relatedTarget:t})},this._dialog,e)}_enforceFocus(){B.off(document,"focusin.bs.modal"),B.on(document,"focusin.bs.modal",t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?B.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):B.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?B.on(window,"resize.bs.modal",()=>this._adjustDialog()):B.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._scrollBar.reset(),B.trigger(this._element,"hidden.bs.modal")})}_showBackdrop(t){B.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())}),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(B.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:s}=this._element,i=e>document.documentElement.clientHeight;!i&&"hidden"===s.overflowY||t.contains("modal-static")||(i||(s.overflowY="hidden"),t.add("modal-static"),this._queueCallback(()=>{t.remove("modal-static"),i||this._queueCallback(()=>{s.overflowY=""},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),s=e>0;(!s&&t&&!v()||s&&!t&&v())&&(this._element.style.paddingLeft=e+"px"),(s&&!t&&!v()||!s&&t&&v())&&(this._element.style.paddingRight=e+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const s=wt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===s[t])throw new TypeError(`No method named "${t}"`);s[t](e)}}))}}B.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=a(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),B.one(e,"show.bs.modal",t=>{t.defaultPrevented||B.one(e,"hidden.bs.modal",()=>{u(this)&&this.focus()})}),wt.getOrCreateInstance(e).toggle(this)})),y(wt);const Et={backdrop:!0,keyboard:!0,scroll:!1},At={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Tt extends q{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._addEventListeners()}static get NAME(){return"offcanvas"}static get Default(){return Et}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||B.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||((new ft).hide(),this._enforceFocusOnElement(this._element)),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),this._queueCallback(()=>{B.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(B.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(B.off(document,"focusin.bs.offcanvas"),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),this._backdrop.hide(),this._queueCallback(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new ft).reset(),B.trigger(this._element,"hidden.bs.offcanvas")},this._element,!0)))}dispose(){this._backdrop.dispose(),super.dispose(),B.off(document,"focusin.bs.offcanvas")}_getConfig(t){return t={...Et,...V.getDataAttributes(this._element),..."object"==typeof t?t:{}},d("offcanvas",t,At),t}_initializeBackDrop(){return new bt({isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_enforceFocusOnElement(t){B.off(document,"focusin.bs.offcanvas"),B.on(document,"focusin.bs.offcanvas",e=>{document===e.target||t===e.target||t.contains(e.target)||t.focus()}),t.focus()}_addEventListeners(){B.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),B.on(this._element,"keydown.dismiss.bs.offcanvas",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()})}static jQueryInterface(t){return this.each((function(){const e=Tt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}B.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=a(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),g(this))return;B.one(e,"hidden.bs.offcanvas",()=>{u(this)&&this.focus()});const s=i.findOne(".offcanvas.show");s&&s!==e&&Tt.getInstance(s).hide(),Tt.getOrCreateInstance(e).toggle(this)})),B.on(window,"load.bs.offcanvas.data-api",()=>i.find(".offcanvas.show").forEach(t=>Tt.getOrCreateInstance(t).show())),y(Tt);const Ct=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),kt=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,Lt=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ot=(t,e)=>{const s=t.nodeName.toLowerCase();if(e.includes(s))return!Ct.has(s)||Boolean(kt.test(t.nodeValue)||Lt.test(t.nodeValue));const i=e.filter(t=>t instanceof RegExp);for(let t=0,e=i.length;t<e;t++)if(i[t].test(s))return!0;return!1};function Dt(t,e,s){if(!t.length)return t;if(s&&"function"==typeof s)return s(t);const i=(new window.DOMParser).parseFromString(t,"text/html"),n=Object.keys(e),o=[].concat(...i.body.querySelectorAll("*"));for(let t=0,s=o.length;t<s;t++){const s=o[t],i=s.nodeName.toLowerCase();if(!n.includes(i)){s.remove();continue}const r=[].concat(...s.attributes),a=[].concat(e["*"]||[],e[i]||[]);r.forEach(t=>{Ot(t,a)||s.removeAttribute(t.nodeName)})}return i.body.innerHTML}const It=new RegExp("(^|\\s)bs-tooltip\\S+","g"),Nt=new Set(["sanitize","allowList","sanitizeFn"]),St={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},xt={AUTO:"auto",TOP:"top",RIGHT:v()?"left":"right",BOTTOM:"bottom",LEFT:v()?"right":"left"},Mt={animation:!0,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Pt={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class jt extends q{constructor(t,e){if(void 0===s)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return Mt}static get NAME(){return"tooltip"}static get Event(){return Pt}static get DefaultType(){return St}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),B.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.remove(),this._popper&&this._popper.destroy(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=B.trigger(this._element,this.constructor.Event.SHOW),e=p(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;const o=this.getTipElement(),r=n(this.constructor.NAME);o.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this._config.animation&&o.classList.add("fade");const a="function"==typeof this._config.placement?this._config.placement.call(this,o,this._element):this._config.placement,l=this._getAttachment(a);this._addAttachmentClass(l);const{container:c}=this._config;W.set(o,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(c.appendChild(o),B.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=s.createPopper(this._element,o,this._getPopperConfig(l)),o.classList.add("show");const h="function"==typeof this._config.customClass?this._config.customClass():this._config.customClass;h&&o.classList.add(...h.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{B.on(t,"mouseover",f)});const d=this.tip.classList.contains("fade");this._queueCallback(()=>{const t=this._hoverState;this._hoverState=null,B.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)},this.tip,d)}hide(){if(!this._popper)return;const t=this.getTipElement();if(B.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>B.off(t,"mouseover",f)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains("fade");this._queueCallback(()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),B.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))},this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");return t.innerHTML=this._config.template,this.tip=t.children[0],this.tip}setContent(){const t=this.getTipElement();this.setElementContent(i.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")}setElementContent(t,e){if(null!==t)return c(e)?(e=h(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Dt(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this._config.title?this._config.title.call(this._element):this._config.title),t}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){const s=this.constructor.DATA_KEY;return(e=e||W.get(t.delegateTarget,s))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),W.set(t.delegateTarget,s,e)),e}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))}_getAttachment(t){return xt[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach(t=>{if("click"===t)B.on(this._element,this.constructor.Event.CLICK,this._config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,s="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;B.on(this._element,e,this._config.selector,t=>this._enter(t)),B.on(this._element,s,this._config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},B.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e._config.delay&&e._config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e._config.delay&&e._config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=V.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{Nt.has(t)&&delete e[t]}),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:h(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),d("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=Dt(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};if(this._config)for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(It);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){const e=jt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}y(jt);const Ht=new RegExp("(^|\\s)bs-popover\\S+","g"),Rt={...jt.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'},Bt={...jt.DefaultType,content:"(string|element|function)"},$t={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Wt extends jt{static get Default(){return Rt}static get NAME(){return"popover"}static get Event(){return $t}static get DefaultType(){return Bt}isWithContent(){return this.getTitle()||this._getContent()}getTipElement(){return this.tip||(this.tip=super.getTipElement(),this.getTitle()||i.findOne(".popover-header",this.tip).remove(),this._getContent()||i.findOne(".popover-body",this.tip).remove()),this.tip}setContent(){const t=this.getTipElement();this.setElementContent(i.findOne(".popover-header",t),this.getTitle());let e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(i.findOne(".popover-body",t),e),t.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this._config.content}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Ht);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}static jQueryInterface(t){return this.each((function(){const e=Wt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}y(Wt);const qt={offset:10,method:"auto",target:""},zt={offset:"number",method:"string",target:"(string|element)"};class Ft extends q{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} .nav-link, ${this._config.target} .list-group-item, ${this._config.target} .dropdown-item`,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,B.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return qt}static get NAME(){return"scrollspy"}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":"position",e="auto"===this._config.method?t:this._config.method,s="position"===e?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),i.find(this._selector).map(t=>{const n=r(t),o=n?i.findOne(n):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[V[e](o).top+s,n]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){B.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){if("string"!=typeof(t={...qt,...V.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target&&c(t.target)){let{id:e}=t.target;e||(e=n("scrollspy"),t.target.id=e),t.target="#"+e}return d("scrollspy",t,zt),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),s=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=s){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t<this._offsets[0]&&this._offsets[0]>0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t<this._offsets[e+1])&&this._activate(this._targets[e])}}_activate(t){this._activeTarget=t,this._clear();const e=this._selector.split(",").map(e=>`${e}[data-bs-target="${t}"],${e}[href="${t}"]`),s=i.findOne(e.join(","));s.classList.contains("dropdown-item")?(i.findOne(".dropdown-toggle",s.closest(".dropdown")).classList.add("active"),s.classList.add("active")):(s.classList.add("active"),i.parents(s,".nav, .list-group").forEach(t=>{i.prev(t,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),i.prev(t,".nav-item").forEach(t=>{i.children(t,".nav-link").forEach(t=>t.classList.add("active"))})})),B.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){i.find(this._selector).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){const e=Ft.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}B.on(window,"load.bs.scrollspy.data-api",()=>{i.find('[data-bs-spy="scroll"]').forEach(t=>new Ft(t))}),y(Ft);class Ut extends q{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active"))return;let t;const e=a(this._element),s=this._element.closest(".nav, .list-group");if(s){const e="UL"===s.nodeName||"OL"===s.nodeName?":scope > li > .active":".active";t=i.find(e,s),t=t[t.length-1]}const n=t?B.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(B.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==n&&n.defaultPrevented)return;this._activate(this._element,s);const o=()=>{B.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),B.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,s){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?i.children(e,".active"):i.find(":scope > li > .active",e))[0],o=s&&n&&n.classList.contains("fade"),r=()=>this._transitionComplete(t,n,s);n&&o?(n.classList.remove("show"),this._queueCallback(r,t,!0)):r()}_transitionComplete(t,e,s){if(e){e.classList.remove("active");const t=i.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),m(t),t.classList.contains("fade")&&t.classList.add("show");let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&i.find(".dropdown-toggle",e).forEach(t=>t.classList.add("active")),t.setAttribute("aria-expanded",!0)}s&&s()}static jQueryInterface(t){return this.each((function(){const e=Ut.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}B.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),g(this)||Ut.getOrCreateInstance(this).show()})),y(Ut);const Kt={animation:"boolean",autohide:"boolean",delay:"number"},Vt={animation:!0,autohide:!0,delay:5e3};class Qt extends q{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Kt}static get Default(){return Vt}static get NAME(){return"toast"}show(){B.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),m(this._element),this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),this._element.classList.add("show"),B.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this._element.classList.contains("show")&&(B.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.remove("show"),this._queueCallback(()=>{this._element.classList.add("hide"),B.trigger(this._element,"hidden.bs.toast")},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),super.dispose()}_getConfig(t){return t={...Vt,...V.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},d("toast",t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const s=t.relatedTarget;this._element===s||this._element.contains(s)||this._maybeScheduleHide()}_setListeners(){B.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',()=>this.hide()),B.on(this._element,"mouseover.bs.toast",t=>this._onInteraction(t,!0)),B.on(this._element,"mouseout.bs.toast",t=>this._onInteraction(t,!1)),B.on(this._element,"focusin.bs.toast",t=>this._onInteraction(t,!0)),B.on(this._element,"focusout.bs.toast",t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Qt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return y(Qt),{Alert:z,Button:F,Carousel:et,Collapse:nt,Dropdown:pt,Modal:wt,Offcanvas:Tt,Popover:Wt,ScrollSpy:Ft,Tab:Ut,Toast:Qt,Tooltip:jt}}));
+//# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/inverter.js b/localwebsite/htdocs/assets/inverter.js
new file mode 100644
index 0000000..72d985c
--- /dev/null
+++ b/localwebsite/htdocs/assets/inverter.js
@@ -0,0 +1,15 @@
+var Inverter = {
+ poll: function () {
+ setInterval(this._tick, 1000);
+ },
+
+ _tick: function() {
+ ajax.get('/inverter/status.ajax')
+ .then(({response}) => {
+ if (response) {
+ var el = document.getElementById('inverter_status');
+ el.innerHTML = response.html;
+ }
+ });
+ }
+}; \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/modem.js b/localwebsite/htdocs/assets/modem.js
new file mode 100644
index 0000000..2ea4264
--- /dev/null
+++ b/localwebsite/htdocs/assets/modem.js
@@ -0,0 +1,29 @@
+var ModemStatus = {
+ _modems: [],
+
+ init: function(modems) {
+ for (var i = 0; i < modems.length; i++) {
+ var modem = modems[i];
+ this._modems.push(new ModemStatusUpdater(modem));
+ }
+ }
+};
+
+
+function ModemStatusUpdater(id) {
+ this.id = id;
+ this.elem = ge('modem_data_'+id);
+ this.fetch();
+}
+extend(ModemStatusUpdater.prototype, {
+ fetch: function() {
+ ajax.get('/modem/status/get.ajax', {
+ id: this.id
+ }).then(({response}) => {
+ var {html} = response;
+ this.elem.innerHTML = html;
+
+ // TODO enqueue rerender
+ });
+ },
+}); \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/polyfills.js b/localwebsite/htdocs/assets/polyfills.js
new file mode 100644
index 0000000..e851999
--- /dev/null
+++ b/localwebsite/htdocs/assets/polyfills.js
@@ -0,0 +1,560 @@
+if (typeof Object.assign != 'function') {
+ // Must be writable: true, enumerable: false, configurable: true
+ Object.defineProperty(Object, "assign", {
+ value: function assign(target, varArgs) { // .length of function is 2
+ 'use strict';
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++) {
+ var nextSource = arguments[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (var nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ },
+ writable: true,
+ configurable: true
+ });
+}
+
+// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
+if (!Object.keys) {
+ Object.keys = (function() {
+ 'use strict';
+ var hasOwnProperty = Object.prototype.hasOwnProperty,
+ hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
+ dontEnums = [
+ 'toString',
+ 'toLocaleString',
+ 'valueOf',
+ 'hasOwnProperty',
+ 'isPrototypeOf',
+ 'propertyIsEnumerable',
+ 'constructor'
+ ],
+ dontEnumsLength = dontEnums.length;
+
+ return function(obj) {
+ if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
+ throw new TypeError('Object.keys called on non-object');
+ }
+
+ var result = [], prop, i;
+
+ for (prop in obj) {
+ if (hasOwnProperty.call(obj, prop)) {
+ result.push(prop);
+ }
+ }
+
+ if (hasDontEnumBug) {
+ for (i = 0; i < dontEnumsLength; i++) {
+ if (hasOwnProperty.call(obj, dontEnums[i])) {
+ result.push(dontEnums[i]);
+ }
+ }
+ }
+ return result;
+ };
+ }());
+}
+
+// const PromisePolyfill = require('es6-promise').Promise;
+// if (!window.Promise) {
+// window.Promise = PromisePolyfill
+// }
+
+// https://tc39.github.io/ecma262/#sec-array.prototype.find
+if (!Array.prototype.find) {
+ Object.defineProperty(Array.prototype, 'find', {
+ value: function(predicate) {
+ // 1. Let O be ? ToObject(this value).
+ if (this == null) {
+ throw TypeError('"this" is null or not defined');
+ }
+
+ var o = Object(this);
+
+ // 2. Let len be ? ToLength(? Get(O, "length")).
+ var len = o.length >>> 0;
+
+ // 3. If IsCallable(predicate) is false, throw a TypeError exception.
+ if (typeof predicate !== 'function') {
+ throw TypeError('predicate must be a function');
+ }
+
+ // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ var thisArg = arguments[1];
+
+ // 5. Let k be 0.
+ var k = 0;
+
+ // 6. Repeat, while k < len
+ while (k < len) {
+ // a. Let Pk be ! ToString(k).
+ // b. Let kValue be ? Get(O, Pk).
+ // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
+ // d. If testResult is true, return kValue.
+ var kValue = o[k];
+ if (predicate.call(thisArg, kValue, k, o)) {
+ return kValue;
+ }
+ // e. Increase k by 1.
+ k++;
+ }
+
+ // 7. Return undefined.
+ return undefined;
+ },
+ configurable: true,
+ writable: true
+ });
+}
+
+if (!Array.prototype.findIndex) {
+ Array.prototype.findIndex = function(predicate) {
+ if (this == null) {
+ throw new TypeError('Array.prototype.findIndex called on null or undefined');
+ }
+ if (typeof predicate !== 'function') {
+ throw new TypeError('predicate must be a function');
+ }
+ var list = Object(this);
+ var length = list.length >>> 0;
+ var thisArg = arguments[1];
+ var value;
+
+ for (var i = 0; i < length; i++) {
+ value = list[i];
+ if (predicate.call(thisArg, value, i, list)) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+
+if (!Array.prototype.filter) {
+ Array.prototype.filter = function(fun/*, thisArg*/) {
+ 'use strict';
+
+ if (this === void 0 || this === null) {
+ throw new TypeError();
+ }
+
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (typeof fun !== 'function') {
+ throw new TypeError();
+ }
+
+ var res = [];
+ var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
+ for (var i = 0; i < len; i++) {
+ if (i in t) {
+ var val = t[i];
+
+ // NOTE: Technically this should Object.defineProperty at
+ // the next index, as push can be affected by
+ // properties on Object.prototype and Array.prototype.
+ // But that method's new, and collisions should be
+ // rare, so use the more-compatible alternative.
+ if (fun.call(thisArg, val, i, t)) {
+ res.push(val);
+ }
+ }
+ }
+
+ return res;
+ };
+}
+
+if (!String.prototype.trim) {
+ String.prototype.trim = function () {
+ return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
+ };
+}
+
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function (oThis) {
+ if (typeof this !== "function") {
+ // closest thing possible to the ECMAScript 5 internal IsCallable function
+ throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
+ }
+
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function () {},
+ fBound = function () {
+ return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ fNOP.prototype = this.prototype;
+ fBound.prototype = new fNOP();
+
+ return fBound;
+ };
+}
+
+Function.prototype.pbind = function() {
+ var args = Array.prototype.slice.call(arguments);
+ args.unshift(window);
+ return this.bind.apply(this, args);
+};
+
+// Fix for Samsung Browser
+if (!Function.prototype.ToString) {
+ Function.prototype.ToString = function () {
+ return this.toString();
+ }
+}
+
+if (!String.prototype.startsWith) {
+ String.prototype.startsWith = function(searchString, position){
+ position = position || 0;
+ return this.substr(position, searchString.length) === searchString;
+ };
+}
+
+if (!String.prototype.endsWith) {
+ String.prototype.endsWith = function(searchString, position) {
+ var subjectString = this.toString();
+ if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
+ position = subjectString.length;
+ }
+ position -= searchString.length;
+ var lastIndex = subjectString.lastIndexOf(searchString, position);
+ return lastIndex !== -1 && lastIndex === position;
+ };
+}
+
+// https://tc39.github.io/ecma262/#sec-array.prototype.includes
+if (!Array.prototype.includes) {
+ Object.defineProperty(Array.prototype, 'includes', {
+ value: function(searchElement, fromIndex) {
+
+ // 1. Let O be ? ToObject(this value).
+ if (this == null) {
+ throw new TypeError('"this" is null or not defined');
+ }
+
+ var o = Object(this);
+
+ // 2. Let len be ? ToLength(? Get(O, "length")).
+ var len = o.length >>> 0;
+
+ // 3. If len is 0, return false.
+ if (len === 0) {
+ return false;
+ }
+
+ // 4. Let n be ? ToInteger(fromIndex).
+ // (If fromIndex is undefined, this step produces the value 0.)
+ var n = fromIndex | 0;
+
+ // 5. If n ≥ 0, then
+ // a. Let k be n.
+ // 6. Else n < 0,
+ // a. Let k be len + n.
+ // b. If k < 0, let k be 0.
+ var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
+
+ // 7. Repeat, while k < len
+ while (k < len) {
+ // a. Let elementK be the result of ? Get(O, ! ToString(k)).
+ // b. If SameValueZero(searchElement, elementK) is true, return true.
+ // c. Increase k by 1.
+ // NOTE: === provides the correct "SameValueZero" comparison needed here.
+ if (o[k] === searchElement) {
+ return true;
+ }
+ k++;
+ }
+
+ // 8. Return false
+ return false;
+ }
+ });
+}
+
+// Шаги алгоритма ECMA-262, 5-е издание, 15.4.4.21
+// Ссылка (en): http://es5.github.io/#x15.4.4.21
+// Ссылка (ru): http://es5.javascript.ru/x15.4.html#x15.4.4.21
+if (!Array.prototype.reduce) {
+ Array.prototype.reduce = function(callback/*, initialValue*/) {
+ 'use strict';
+ if (this == null) {
+ throw new TypeError('Array.prototype.reduce called on null or undefined');
+ }
+ if (typeof callback !== 'function') {
+ throw new TypeError(callback + ' is not a function');
+ }
+ var t = Object(this), len = t.length >>> 0, k = 0, value;
+ if (arguments.length >= 2) {
+ value = arguments[1];
+ } else {
+ while (k < len && ! (k in t)) {
+ k++;
+ }
+ if (k >= len) {
+ throw new TypeError('Reduce of empty array with no initial value');
+ }
+ value = t[k++];
+ }
+ for (; k < len; k++) {
+ if (k in t) {
+ value = callback(value, t[k], k, t);
+ }
+ }
+ return value;
+ };
+}
+
+
+Array.prototype.pushOnce = function(value) {
+ if (!this.includes(value)) {
+ this.push(value)
+ }
+}
+
+Array.prototype.removeOnce = function(value) {
+ let index = this.indexOf(value)
+ if (index !== -1) {
+ this.splice(index, 1)
+ }
+}
+
+// Production steps of ECMA-262, Edition 5, 15.4.4.18
+// Reference: http://es5.github.io/#x15.4.4.18
+if (!Array.prototype.forEach) {
+
+ Array.prototype.forEach = function(callback/*, thisArg*/) {
+
+ var T, k;
+
+ if (this == null) {
+ throw new TypeError('this is null or not defined');
+ }
+
+ // 1. Let O be the result of calling toObject() passing the
+ // |this| value as the argument.
+ var O = Object(this);
+
+ // 2. Let lenValue be the result of calling the Get() internal
+ // method of O with the argument "length".
+ // 3. Let len be toUint32(lenValue).
+ var len = O.length >>> 0;
+
+ // 4. If isCallable(callback) is false, throw a TypeError exception.
+ // See: http://es5.github.com/#x9.11
+ if (typeof callback !== 'function') {
+ throw new TypeError(callback + ' is not a function');
+ }
+
+ // 5. If thisArg was supplied, let T be thisArg; else let
+ // T be undefined.
+ if (arguments.length > 1) {
+ T = arguments[1];
+ }
+
+ // 6. Let k be 0.
+ k = 0;
+
+ // 7. Repeat while k < len.
+ while (k < len) {
+
+ var kValue;
+
+ // a. Let Pk be ToString(k).
+ // This is implicit for LHS operands of the in operator.
+ // b. Let kPresent be the result of calling the HasProperty
+ // internal method of O with argument Pk.
+ // This step can be combined with c.
+ // c. If kPresent is true, then
+ if (k in O) {
+
+ // i. Let kValue be the result of calling the Get internal
+ // method of O with argument Pk.
+ kValue = O[k];
+
+ // ii. Call the Call internal method of callback with T as
+ // the this value and argument list containing kValue, k, and O.
+ callback.call(T, kValue, k, O);
+ }
+ // d. Increase k by 1.
+ k++;
+ }
+ // 8. return undefined.
+ };
+}
+
+
+if (!Array.isArray) {
+ Array.isArray = function(arg) {
+ return Object.prototype.toString.call(arg) === '[object Array]';
+ };
+}
+
+
+// Production steps of ECMA-262, Edition 6, 22.1.2.1
+if (!Array.from) {
+ Array.from = (function () {
+ var toStr = Object.prototype.toString;
+ var isCallable = function (fn) {
+ return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
+ };
+ var toInteger = function (value) {
+ var number = Number(value);
+ if (isNaN(number)) { return 0; }
+ if (number === 0 || !isFinite(number)) { return number; }
+ return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
+ };
+ var maxSafeInteger = Math.pow(2, 53) - 1;
+ var toLength = function (value) {
+ var len = toInteger(value);
+ return Math.min(Math.max(len, 0), maxSafeInteger);
+ };
+
+ // The length property of the from method is 1.
+ return function from(arrayLike/*, mapFn, thisArg */) {
+ // 1. Let C be the this value.
+ var C = this;
+
+ // 2. Let items be ToObject(arrayLike).
+ var items = Object(arrayLike);
+
+ // 3. ReturnIfAbrupt(items).
+ if (arrayLike == null) {
+ throw new TypeError('Array.from requires an array-like object - not null or undefined');
+ }
+
+ // 4. If mapfn is undefined, then let mapping be false.
+ var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
+ var T;
+ if (typeof mapFn !== 'undefined') {
+ // 5. else
+ // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
+ if (!isCallable(mapFn)) {
+ throw new TypeError('Array.from: when provided, the second argument must be a function');
+ }
+
+ // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (arguments.length > 2) {
+ T = arguments[2];
+ }
+ }
+
+ // 10. Let lenValue be Get(items, "length").
+ // 11. Let len be ToLength(lenValue).
+ var len = toLength(items.length);
+
+ // 13. If IsConstructor(C) is true, then
+ // 13. a. Let A be the result of calling the [[Construct]] internal method
+ // of C with an argument list containing the single item len.
+ // 14. a. Else, Let A be ArrayCreate(len).
+ var A = isCallable(C) ? Object(new C(len)) : new Array(len);
+
+ // 16. Let k be 0.
+ var k = 0;
+ // 17. Repeat, while k < len… (also steps a - h)
+ var kValue;
+ while (k < len) {
+ kValue = items[k];
+ if (mapFn) {
+ A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
+ } else {
+ A[k] = kValue;
+ }
+ k += 1;
+ }
+ // 18. Let putStatus be Put(A, "length", len, true).
+ A.length = len;
+ // 20. Return A.
+ return A;
+ };
+ }());
+}
+
+// Production steps of ECMA-262, Edition 5, 15.4.4.17
+// Reference: https://es5.github.io/#x15.4.4.17
+if (!Array.prototype.some) {
+ Array.prototype.some = function(fun, thisArg) {
+ 'use strict';
+
+ if (this == null) {
+ throw new TypeError('Array.prototype.some called on null or undefined');
+ }
+
+ if (typeof fun !== 'function') {
+ throw new TypeError();
+ }
+
+ var t = Object(this);
+ var len = t.length >>> 0;
+
+ for (var i = 0; i < len; i++) {
+ if (i in t && fun.call(thisArg, t[i], i, t)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+}
+
+/**
+ * String.padEnd()
+ * version 1.0.1
+ * Feature Chrome Firefox Internet Explorer Opera Safari Edge
+ * Basic support 57 48 (No) 44 10 15
+ * -------------------------------------------------------------------------------
+ */
+if (!String.prototype.padEnd) {
+ String.prototype.padEnd = function padEnd(targetLength, padString) {
+ targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
+ padString = String(typeof padString !== 'undefined' ? padString : ' ');
+ if (this.length > targetLength) {
+ return String(this);
+ } else {
+ targetLength = targetLength - this.length;
+ if (targetLength > padString.length) {
+ padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
+ }
+ return String(this) + padString.slice(0, targetLength);
+ }
+ };
+}
+
+/**
+ * String.padStart()
+ * version 1.0.1
+ * Feature Chrome Firefox Internet Explorer Opera Safari Edge
+ * Basic support 57 51 (No) 44 10 15
+ * -------------------------------------------------------------------------------
+ */
+if (!String.prototype.padStart) {
+ String.prototype.padStart = function padStart(targetLength, padString) {
+ targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
+ padString = String(typeof padString !== 'undefined' ? padString : ' ');
+ if (this.length > targetLength) {
+ return String(this);
+ } else {
+ targetLength = targetLength - this.length;
+ if (targetLength > padString.length) {
+ padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
+ }
+ return padString.slice(0, targetLength) + String(this);
+ }
+ };
+} \ No newline at end of file
diff --git a/localwebsite/htdocs/favicon.ico b/localwebsite/htdocs/favicon.ico
new file mode 100644
index 0000000..d5ff579
--- /dev/null
+++ b/localwebsite/htdocs/favicon.ico
Binary files differ
diff --git a/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php
new file mode 100644
index 0000000..a825fcb
--- /dev/null
+++ b/localwebsite/htdocs/index.php
@@ -0,0 +1,44 @@
+<?php
+
+require_once __DIR__.'/../init.php';
+
+global $lang;
+
+$router = new router;
+
+// modem
+$router->add('modem/status/', 'Modem status_page');
+$router->add('modem/status/get.ajax', 'Modem status_get_ajax');
+
+$router->add('routing/', 'Modem routing_smallhome_page');
+$router->add('routing/switch-small-home/', 'Modem routing_smallhome_switch');
+$router->add('routing/{ipsets,dhcp}/', 'Modem routing_${1}_page');
+$router->add('routing/ipsets/{add,del}/', 'Modem routing_ipsets_${1}');
+
+$router->add('modem/sms/', 'Modem sms_page');
+// $router->add('modem/set.ajax', 'Modem ctl_set_ajax');
+
+// inverter
+$router->add('inverter/', 'Inverter status_page');
+$router->add('inverter/status.ajax', 'Inverter status_ajax');
+
+// misc
+$router->add('/', 'Misc main');
+$router->add('sensors/', 'Misc sensors_page');
+$router->add('pump/', 'Misc pump_page');
+$router->add('phpinfo/', 'Misc phpinfo');
+
+
+$route = routerFind($router);
+if ($route === false)
+ (new FakeRequestHandler)->dispatch('404');
+
+list($handler, $act, $RouterInput) = $route;
+
+$handler_class = $handler.'Handler';
+if (!class_exists($handler_class)) {
+ debugError('index.php: class '.$handler_class.' not found');
+ (new FakeRequestHandler)->dispatch('404');
+}
+
+(new $handler_class)->dispatch($act);
diff --git a/localwebsite/init.php b/localwebsite/init.php
new file mode 100644
index 0000000..2eafcf3
--- /dev/null
+++ b/localwebsite/init.php
@@ -0,0 +1,71 @@
+<?php
+
+error_reporting(E_ALL);
+ini_set('display_errors', 1);
+
+mb_internal_encoding('UTF-8');
+mb_regex_encoding('UTF-8');
+
+register_shutdown_function(function() {
+ global $ShutdownFunctions;
+ if (!empty($ShutdownFunctions)) {
+ foreach ($ShutdownFunctions as $f)
+ $f();
+ }
+});
+
+spl_autoload_register(function($class) {
+ if (endsWith($class, 'Handler'))
+ $path = ROOT.'/handlers/'.$class.'.php';
+
+ // engine classes
+ else if (in_array($class, ['request_handler', 'router', 'model', 'debug']))
+ $path = ROOT.'/engine/'.$class.'.php';
+
+ else if ($class == 'Lang')
+ $path = ROOT.'/engine/lang.php';
+
+ else if (endsWith($class, '_tpl'))
+ $path = ROOT.'/engine/tpl.php';
+
+ // other classes
+ else
+ $path = ROOT.'/classes/'.$class.'.php';
+
+ if (strpos($path, '\\') !== false)
+ $path = str_replace('\\', '/', $path);
+
+ if (is_file($path))
+ require_once $path;
+});
+
+define('ROOT', __DIR__);
+define('START_TIME', microtime(true));
+
+set_include_path(get_include_path().PATH_SEPARATOR.ROOT);
+
+$config = require ROOT.'/config.php';
+if (!is_file(ROOT.'/config.local.php'))
+ die('config.local.php not found');
+$config = array_merge($config, require_once ROOT.'/config.local.php');
+
+require_once ROOT.'/functions.php';
+
+// it's better to start logging as early as possible
+$debug = debug::getInstance(
+ function($errno, $errfile, $errlne, $errstr) {
+ // it's not our fault that some vendor package uses something that's deprecated
+ // so let's not spam our logs
+ if ($errno == E_USER_DEPRECATED && startsWith($errfile, ROOT.'/vendor/'))
+ return false;
+
+ return true;
+ }
+);
+$debug->setMessagesStoreType(debug::STORE_FILE);
+$debug->setErrorsStoreType(debug::STORE_FILE);
+$debug->enable();
+unset($debug);
+
+// composer
+require_once ROOT.'/../vendor/autoload.php';
diff --git a/localwebsite/templates-web/404.twig b/localwebsite/templates-web/404.twig
new file mode 100644
index 0000000..e28f5c5
--- /dev/null
+++ b/localwebsite/templates-web/404.twig
@@ -0,0 +1 @@
+Page Not Found \ No newline at end of file
diff --git a/localwebsite/templates-web/footer.twig b/localwebsite/templates-web/footer.twig
new file mode 100644
index 0000000..8739f80
--- /dev/null
+++ b/localwebsite/templates-web/footer.twig
@@ -0,0 +1,8 @@
+{% if js %}
+ <script>{{ js|raw }}</script>
+{% endif %}
+
+</div>
+</body>
+</html>
+<!-- generated in {{ exec_time}} --> \ No newline at end of file
diff --git a/localwebsite/templates-web/header.twig b/localwebsite/templates-web/header.twig
new file mode 100644
index 0000000..03560b0
--- /dev/null
+++ b/localwebsite/templates-web/header.twig
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <title>{{ title }}</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ {{ static|raw }}
+</head>
+<body>
+<div class="container py-3"> \ No newline at end of file
diff --git a/localwebsite/templates-web/index.twig b/localwebsite/templates-web/index.twig
new file mode 100644
index 0000000..d293cfd
--- /dev/null
+++ b/localwebsite/templates-web/index.twig
@@ -0,0 +1,21 @@
+<div class="container py-4">
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item active" aria-current="page">Главная</li>
+ </ol>
+ </nav>
+
+ <h6>Интернет</h6>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item"><a href="/modem/status/">Состояние</a></li>
+ <li class="list-group-item"><a href="/routing/">Маршрутизация</a></li>
+ <li class="list-group-item"><a href="/modem/sms/">SMS-сообщения</a></li>
+ </ul>
+
+ <h6 class="mt-4">Другое</h6>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item"><a href="/inverter/">Инвертор</a></li>
+ <li class="list-group-item"><a href="/pump/">Насос</a></li>
+ <li class="list-group-item"><a href="/sensors/">Датчики</a></li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/localwebsite/templates-web/inverter_page.twig b/localwebsite/templates-web/inverter_page.twig
new file mode 100644
index 0000000..2b0af90
--- /dev/null
+++ b/localwebsite/templates-web/inverter_page.twig
@@ -0,0 +1,15 @@
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"><a href="/">Главная</a></li>
+ <li class="breadcrumb-item active" aria-current="page">Инвертор</li>
+ </ol>
+</nav>
+
+<h6 class="text-primary">Статус</h6>
+<div id="inverter_status">
+ {{ html|raw }}
+</div>
+
+{% js %}
+Inverter.poll();
+{% endjs %} \ No newline at end of file
diff --git a/localwebsite/templates-web/modem_data.twig b/localwebsite/templates-web/modem_data.twig
new file mode 100644
index 0000000..55440ea
--- /dev/null
+++ b/localwebsite/templates-web/modem_data.twig
@@ -0,0 +1,12 @@
+{% if not loading %}
+<span class="text-secondary">Сигнал:</span> {% include 'signal_level.twig' with {'level': modem_data.level} %}<br>
+<span class="text-secondary">Тип сети:</span> <b>{{ modem_data.type }}</b><br>
+<span class="text-secondary">RSSI:</span> {{ modem_data.rssi }}<br/>
+{% if modem_data.sinr %}
+ <span class="text-secondary">SINR:</span> {{ modem_data.sinr }}<br/>
+{% endif %}
+<span class="text-secondary">Время соединения:</span> {{ modem_data.connected_time }}<br>
+<span class="text-secondary">Принято/передано:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }}
+{% else %}
+{% include 'spinner.twig' %}
+{% endif %} \ No newline at end of file
diff --git a/localwebsite/templates-web/modem_status_page.twig b/localwebsite/templates-web/modem_status_page.twig
new file mode 100644
index 0000000..1aa5cf8
--- /dev/null
+++ b/localwebsite/templates-web/modem_status_page.twig
@@ -0,0 +1,19 @@
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"><a href="/">Главная</a></li>
+ <li class="breadcrumb-item active" aria-current="page">Модемы</li>
+ </ol>
+</nav>
+
+{% for modem_key, modem in modems %}
+ <h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modem.label }}</h6>
+ <div id="modem_data_{{ modem_key }}">
+ {% include 'modem_data.twig' with {
+ loading: true
+ } %}
+ </div>
+{% endfor %}
+
+{% js %}
+ModemStatus.init({{ js_modems|json_encode|raw }});
+{% endjs %}
diff --git a/localwebsite/templates-web/pump.twig b/localwebsite/templates-web/pump.twig
new file mode 100644
index 0000000..4a8cad5
--- /dev/null
+++ b/localwebsite/templates-web/pump.twig
@@ -0,0 +1,18 @@
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"><a href="/">Главная</a></li>
+ <li class="breadcrumb-item active" aria-current="page">Насос</li>
+ </ol>
+</nav>
+
+<form action="/pump/" method="get">
+ <input type="hidden" name="set" value="{{ status == 'on' ? 'off' : 'on' }}" />
+ Сейчас насос
+ {% if status == 'on' %}
+ <span class="text-success"><b>включен</b></span>.<br><br>
+ <button type="submit" class="btn btn-primary">Выключить</button>
+ {% else %}
+ <span class="text-danger"><b>выключен</b></span>.<br><br>
+ <button type="submit" class="btn btn-primary">Включить</button>
+ {% endif %}
+</form> \ No newline at end of file
diff --git a/localwebsite/templates-web/routing_dhcp_page.twig b/localwebsite/templates-web/routing_dhcp_page.twig
new file mode 100644
index 0000000..12b837e
--- /dev/null
+++ b/localwebsite/templates-web/routing_dhcp_page.twig
@@ -0,0 +1,11 @@
+{% include 'routing_header.twig' with {
+ selected_tab: 'dhcp'
+} %}
+
+{% for lease in leases %}
+ <div class="mt-3">
+ <b>{{ lease.hostname }}</b> <span class="text-secondary">(exp: {{ lease.time_s }})</span><br/>
+ {{ lease.ip }}<br>
+ <span class="text-secondary">{{ lease.mac }}</span>
+ </div>
+{% endfor %} \ No newline at end of file
diff --git a/localwebsite/templates-web/routing_header.twig b/localwebsite/templates-web/routing_header.twig
new file mode 100644
index 0000000..f7322f9
--- /dev/null
+++ b/localwebsite/templates-web/routing_header.twig
@@ -0,0 +1,24 @@
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"><a href="/">Главная</a></li>
+ <li class="breadcrumb-item active" aria-current="page">Маршрутизация</li>
+ </ol>
+</nav>
+
+{% set routing_tabs = [
+ {tab: 'smallhome', url: '/routing/', label: 'Маленький дом'},
+ {tab: 'ipsets', url: '/routing/ipsets/', label: 'Правила'},
+ {tab: 'dhcp', url: '/routing/dhcp/', label: 'DHCP'}
+] %}
+
+<nav>
+ <div class="nav nav-tabs" id="nav-tab">
+ {% for tab in routing_tabs %}
+ <a href="{{ tab.url }}" class="text-decoration-none"><button class="nav-link{% if tab.tab == selected_tab %} active{% endif %}" type="button">{{ tab.label }}</button></a>
+ {% endfor %}
+ </div>
+</nav>
+
+{% if error %}
+ <div class="mt-4 alert alert-danger"><b>Ошибка:</b> {{ error }}</div>
+{% endif %}
diff --git a/localwebsite/templates-web/routing_ipsets_page.twig b/localwebsite/templates-web/routing_ipsets_page.twig
new file mode 100644
index 0000000..5996e68
--- /dev/null
+++ b/localwebsite/templates-web/routing_ipsets_page.twig
@@ -0,0 +1,29 @@
+{% include 'routing_header.twig' with {
+ selected_tab: 'ipsets'
+} %}
+
+<div class="mt-2 text-secondary">
+ Таблицы расположены в порядке применения правил iptables.
+</div>
+
+{% for set, ips in sets %}
+ <h6 class="text-primary mt-4">{{ set }}</h6>
+
+ {% if ips %}
+ {% for ip in ips %}
+ <div>{{ ip }} (<a href="/routing/ipsets/del/?set={{ set }}&amp;ip={{ ip }}" onclick="return confirm('Подтвердите удаление {{ ip }} из {{ set }}.')">удалить</a>)</div>
+ {% endfor %}
+ {% else %}
+ <span class="text-secondary">Нет записей.</span>
+ {% endif %}
+
+ <div style="max-width: 300px">
+ <form method="post" action="/routing/ipsets/add/">
+ <input type="hidden" name="set" value="{{ set }}">
+ <div class="input-group mt-2">
+ <input type="text" name="ip" placeholder="x.x.x.x/y" class="form-control">
+ <button type="submit" class="btn btn-outline-primary">Добавить</button>
+ </div>
+ </form>
+ </div>
+{% endfor %} \ No newline at end of file
diff --git a/localwebsite/templates-web/routing_page.twig b/localwebsite/templates-web/routing_page.twig
new file mode 100644
index 0000000..90437fd
--- /dev/null
+++ b/localwebsite/templates-web/routing_page.twig
@@ -0,0 +1,17 @@
+{% include 'routing_header.twig' with {
+ selected_tab: 'smallhome'
+} %}
+
+<div class="mt-3 mb-3">
+ Текущий апстрим: <b>{{ current.label }}</b>
+</div>
+
+{% for key, modem in modems %}
+ {% if key != current.key %}
+ <div class="pt-1 pb-2">
+ <a href="/routing/switch-small-home/?upstream={{ key }}">
+ <button type="button" class="btn btn-primary">Переключить на <b>{{ modem.label }}</b></button>
+ </a>
+ </div>
+ {% endif %}
+{% endfor %} \ No newline at end of file
diff --git a/localwebsite/templates-web/sensors.twig b/localwebsite/templates-web/sensors.twig
new file mode 100644
index 0000000..354e4e7
--- /dev/null
+++ b/localwebsite/templates-web/sensors.twig
@@ -0,0 +1,12 @@
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"><a href="/">Главная</a></li>
+ <li class="breadcrumb-item active" aria-current="page">Датчики</li>
+ </ol>
+</nav>
+
+{% for key, sensor in sensors %}
+ <h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ sensor.name }}</h6>
+ <span class="text-secondary">Температура:</span> <b>{{ sensor.temp }}</b> °C<br>
+ <span class="text-secondary">Влажность:</span> <b>{{ sensor.humidity }}</b>%
+{% endfor %}
diff --git a/localwebsite/templates-web/signal_level.twig b/localwebsite/templates-web/signal_level.twig
new file mode 100644
index 0000000..9498482
--- /dev/null
+++ b/localwebsite/templates-web/signal_level.twig
@@ -0,0 +1,5 @@
+<div class="signal_level">
+ {% for i in 0..4 %}
+ <div{% if i < level %} class="yes"{% endif %}></div>
+ {% endfor %}
+</div> \ No newline at end of file
diff --git a/localwebsite/templates-web/sms_page.twig b/localwebsite/templates-web/sms_page.twig
new file mode 100644
index 0000000..b6551a3
--- /dev/null
+++ b/localwebsite/templates-web/sms_page.twig
@@ -0,0 +1,27 @@
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"><a href="/">Главная</a></li>
+ <li class="breadcrumb-item active" aria-current="page">SMS-сообщения</li>
+ </ol>
+</nav>
+
+<nav>
+ <div class="nav nav-tabs" id="nav-tab">
+ {% for modem in modems_list %}
+ {% if selected_modem != modem %}<a href="/modem/sms/?modem={{ modem }}" class="text-decoration-none">{% endif %}
+ <button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ modems[modem].short_label }}</button>
+ {% if selected_modem != modem %}</a>{% endif %}
+ {% endfor %}
+ </div>
+</nav>
+
+<h6 class="text-primary mt-4">Последние входящие</h6>
+
+{% for m in messages %}
+ <div class="mt-3">
+ <b>{{ m.phone }}</b> <span class="text-secondary">({{ m.date }})</span><br/>
+ {{ m.content }}
+ </div>
+{% else %}
+ <span class="text-secondary">Сообщений нет.</span>
+{% endfor %} \ No newline at end of file
diff --git a/localwebsite/templates-web/spinner.twig b/localwebsite/templates-web/spinner.twig
new file mode 100644
index 0000000..2d629ea
--- /dev/null
+++ b/localwebsite/templates-web/spinner.twig
@@ -0,0 +1,14 @@
+<div class="sk-fading-circle">
+ <div class="sk-circle1 sk-circle"></div>
+ <div class="sk-circle2 sk-circle"></div>
+ <div class="sk-circle3 sk-circle"></div>
+ <div class="sk-circle4 sk-circle"></div>
+ <div class="sk-circle5 sk-circle"></div>
+ <div class="sk-circle6 sk-circle"></div>
+ <div class="sk-circle7 sk-circle"></div>
+ <div class="sk-circle8 sk-circle"></div>
+ <div class="sk-circle9 sk-circle"></div>
+ <div class="sk-circle10 sk-circle"></div>
+ <div class="sk-circle11 sk-circle"></div>
+ <div class="sk-circle12 sk-circle"></div>
+</div> \ No newline at end of file