diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a1cedf8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Zeilenenden gehoeren zum Repository, nicht zur Maschine. +# +# Ohne diese Datei entscheidet jede Arbeitsstation fuer sich: Git fuer +# Windows setzt core.autocrlf=input in seiner System-Konfiguration, die NAS +# setzt nichts. Dasselbe Repository, zwei Verhalten - und weil der Bestand +# selbst gemischt war (34 PHP-Dateien mit LF, 30 mit CRLF), gab es kein +# richtiges Zeilenende, an das ein Werkzeug sich haette halten koennen. +# Jede Ergaenzung mit LF in einer CRLF-Datei ergab eine gemischte Datei. +# +# LF und nicht CRLF, weil dieses Verzeichnis der Web-Ordner der NAS ist und +# von Linux ausgeliefert wird. +* text=auto eol=lf + +# Eingekaufter Fremdcode bleibt byteweise so, wie er geliefert wurde - dann +# bleibt ein Update dagegen vergleichbar. Darunter liegen auch 292 +# Zertifikate, die niemand anfassen sollte. +restricted/WebAuthn/** -text diff --git a/addUser.php b/addUser.php index 2411841..30be75d 100644 --- a/addUser.php +++ b/addUser.php @@ -1,16 +1,16 @@ - \ No newline at end of file diff --git a/ajax/getConsData.php b/ajax/getConsData.php index 423c134..c8688c8 100644 --- a/ajax/getConsData.php +++ b/ajax/getConsData.php @@ -1,162 +1,162 @@ -".mysqli_error($mysql)."
"; - } - $obj = (object)[]; // Cast empty array to object - $obj->labels = []; - $obj->datasets = []; - $i = 0; - $filled = 0; - if ($simRes->num_rows > 1) { - $dataset = (object)[]; - $row1 = $simRes->fetch_assoc(); - $dataset->borderColor = $linecolors["Vorhersage"]; - $dataset->backgroundColor = $linecolors["Vorhersage"]."55"; - $dataset->borderWidth=1.5; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - $dataset->stack = "sim"; - $dataset->fill = "none"; - $dataset->yAxisID = 'y'; - $dataset->label = "Vorhersage"; - /*$pt = (object)[]; - $pt->x = $row1["time"]*1000; - $pt->y = $row1["Vorhersage"]; - $dataset->data[] = clone $pt; - while ($row1 = $simRes->fetch_assoc()) { - $pt = (object)[]; - $pt->x = $row1["time"]*1000 + 30*60*1000; - $pt->y = $row1["Vorhersage"]; - $dataset->data[] = clone $pt; - }*/ - $obj->datasets[] = clone $dataset; - $rownext = $simRes->fetch_assoc(); - $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; - } - if ($result->num_rows > 1) { - $ii = 1; - $row = $result->fetch_assoc(); - foreach ($row as $name => $value) { - $dataset = (object)[]; - if ($name != "time") { - $dataset->borderColor = $linecolors[$name]; - $dataset->backgroundColor = $linecolors[$name]."55"; - $dataset->borderWidth=1; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - if ($name == "Solarleistung") { - $dataset->stack = "SolarPwr"; - $dataset->fill = "none"; - $dataset->yAxisID = 'y'; - } else if ($name == "Ladestand") { - $dataset->stack = "Charge"; - $dataset->fill = "none"; - $dataset->yAxisID = 'y1'; - } else { - $dataset->stack = "Consumers"; - if ($filled == 0) { - $filled = 1; - $dataset->fill = "origin"; - } else { - $dataset->fill = "-1"; - } - $dataset->yAxisID = 'y'; - } - $dataset->label = $name; - - $dataset->data[] = $value; - $obj->datasets[] = clone $dataset; - $ii++; - } else { - $obj->labels[] = $value * 1000; - } - } - while ($row = $result->fetch_assoc()) { - $ii = 1; - foreach ($row as $name => $value) { - if ($name != "time") { - $obj->datasets[$ii]->data[] = $value; - $ii++; - } else { - if(($value * 1000) < $nextSimTimestamp){ - $obj->datasets[0]->data[] = $row1["Vorhersage"]; - }else{ - $row1 = $rownext; - $rownext = $simRes->fetch_assoc(); - $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; - $obj->datasets[0]->data[] = $row1["Vorhersage"]; - } - $obj->labels[] = $value * 1000; - } - } - } - } - $obj->labels[] = $nextSimTimestamp; //Draw future forecast - $obj->datasets[0]->data[] = $rownext["Vorhersage"]; - while($rownext = $simRes->fetch_assoc()){ - $obj->labels[] = $rownext["time"]*1000 + 30*60*1000; - $obj->datasets[0]->data[] = $rownext["Vorhersage"]; - } - - -} - - -//header('Content-Type: application/json'); -echo json_encode($obj); -//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; +".mysqli_error($mysql)."
"; + } + $obj = (object)[]; // Cast empty array to object + $obj->labels = []; + $obj->datasets = []; + $i = 0; + $filled = 0; + if ($simRes->num_rows > 1) { + $dataset = (object)[]; + $row1 = $simRes->fetch_assoc(); + $dataset->borderColor = $linecolors["Vorhersage"]; + $dataset->backgroundColor = $linecolors["Vorhersage"]."55"; + $dataset->borderWidth=1.5; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + $dataset->stack = "sim"; + $dataset->fill = "none"; + $dataset->yAxisID = 'y'; + $dataset->label = "Vorhersage"; + /*$pt = (object)[]; + $pt->x = $row1["time"]*1000; + $pt->y = $row1["Vorhersage"]; + $dataset->data[] = clone $pt; + while ($row1 = $simRes->fetch_assoc()) { + $pt = (object)[]; + $pt->x = $row1["time"]*1000 + 30*60*1000; + $pt->y = $row1["Vorhersage"]; + $dataset->data[] = clone $pt; + }*/ + $obj->datasets[] = clone $dataset; + $rownext = $simRes->fetch_assoc(); + $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; + } + if ($result->num_rows > 1) { + $ii = 1; + $row = $result->fetch_assoc(); + foreach ($row as $name => $value) { + $dataset = (object)[]; + if ($name != "time") { + $dataset->borderColor = $linecolors[$name]; + $dataset->backgroundColor = $linecolors[$name]."55"; + $dataset->borderWidth=1; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + if ($name == "Solarleistung") { + $dataset->stack = "SolarPwr"; + $dataset->fill = "none"; + $dataset->yAxisID = 'y'; + } else if ($name == "Ladestand") { + $dataset->stack = "Charge"; + $dataset->fill = "none"; + $dataset->yAxisID = 'y1'; + } else { + $dataset->stack = "Consumers"; + if ($filled == 0) { + $filled = 1; + $dataset->fill = "origin"; + } else { + $dataset->fill = "-1"; + } + $dataset->yAxisID = 'y'; + } + $dataset->label = $name; + + $dataset->data[] = $value; + $obj->datasets[] = clone $dataset; + $ii++; + } else { + $obj->labels[] = $value * 1000; + } + } + while ($row = $result->fetch_assoc()) { + $ii = 1; + foreach ($row as $name => $value) { + if ($name != "time") { + $obj->datasets[$ii]->data[] = $value; + $ii++; + } else { + if(($value * 1000) < $nextSimTimestamp){ + $obj->datasets[0]->data[] = $row1["Vorhersage"]; + }else{ + $row1 = $rownext; + $rownext = $simRes->fetch_assoc(); + $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; + $obj->datasets[0]->data[] = $row1["Vorhersage"]; + } + $obj->labels[] = $value * 1000; + } + } + } + } + $obj->labels[] = $nextSimTimestamp; //Draw future forecast + $obj->datasets[0]->data[] = $rownext["Vorhersage"]; + while($rownext = $simRes->fetch_assoc()){ + $obj->labels[] = $rownext["time"]*1000 + 30*60*1000; + $obj->datasets[0]->data[] = $rownext["Vorhersage"]; + } + + +} + + +//header('Content-Type: application/json'); +echo json_encode($obj); +//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; ?> \ No newline at end of file diff --git a/ajax/getForecastData.php b/ajax/getForecastData.php index 1729184..5093ba3 100644 --- a/ajax/getForecastData.php +++ b/ajax/getForecastData.php @@ -1,171 +1,171 @@ -= DATE(DATE_SUB(NOW(),INTERVAL 28 DAY)) And DATE(datetime) != DATE(NOW()) - GROUP BY WEEKDAY(datetime) -ORDER BY datetime"; - -$prodEstQuery = "SELECT UNIX_TIMESTAMP(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) AS 'time', - SUM(power*500) AS 'Vorhersage' - FROM simPower - WHERE DATE(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) >= DATE(DATE_SUB(NOW(),INTERVAL 7 DAY)) - GROUP BY DAY(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) - ORDER BY period_End;"; - -$prodRealQuery = "SELECT - UNIX_TIMESTAMP(datetime) AS 'time', - SUM(pvP/12) AS 'Tatsächliche Erzeugung', - SUM(-totalConsumption/12) AS 'Tatsächlicher Verbrauch' -FROM EnergyFlow -WHERE - DATE(datetime) >= DATE(DATE_SUB(NOW(),INTERVAL 7 DAY)) - GROUP BY DAY(datetime) -ORDER BY datetime"; - - -$linecolors["Tatsächliche Erzeugung"] = "#cccc00"; -$linecolors["Tatsächlicher Verbrauch"] = "#EE9900"; -$linecolors["Vorraussichtl. Verbrauch"] = "#BB4400"; -$linecolors["Auto UG"] = "#00aaFF"; -$linecolors["Auto OG"] = "#0044FF"; -$linecolors["Heizstab"] = "#FF0000"; -$linecolors["Batterieladung"] = "#00aa00"; -$linecolors["Einspeisung"] = "#b0b0b0"; -$linecolors["Ladestand"] = "#00aa00"; -$linecolors["Vorhersage"] = "#4444FF"; - - - -if (checkLogin()) { - $mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB); - $consEst = mysqli_query($mysql, $consEstQuery); - $prodEst = mysqli_query($mysql,$prodEstQuery); - $prodReal = mysqli_query($mysql,$prodRealQuery); - if(!$consEst){ - echo "Error:
".mysqli_error($mysql)."
"; - } - if(!$prodEst){ - echo "Error:
".mysqli_error($mysql)."
"; - } - if(!$prodRealQuery){ - echo "Error:
".mysqli_error($mysql)."
"; - } - - $obj = (object)[]; // Cast empty array to object - $obj->labels = []; - $obj->datasets = []; - $i = 0; - $filled = 0; - - if($consEst->num_rows > 1){ - $row = $consEst->fetch_assoc(); - foreach ($row as $name => $value) { - $dataset = (object)[]; - if ($name != "time") { - $dataset->borderColor = $linecolors[$name]; - $dataset->backgroundColor = $linecolors[$name]."66"; - $dataset->borderWidth=1; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - $dataset->stack = $name; - //$dataset->fill = "none"; - $dataset->yAxisID = 'y'; - $dataset->label = $name; - for($i=0;$i<7;$i++){ - $dataset->data[] = NULL; - } - $dataset->data[] = $value; - $obj->datasets[] = clone $dataset; - } - } - while ($row = $consEst->fetch_assoc()) { - $ii = 0; - foreach ($row as $name => $value) { - if ($name != "time") { - $obj->datasets[$ii]->data[] = $value; - $ii++; - } - } - } - } - - if ($prodEst->num_rows > 1) { - $row = $prodEst->fetch_assoc(); - foreach ($row as $name => $value) { - $dataset = (object)[]; - if ($name != "time") { - $dataset->borderColor = $linecolors[$name]; - $dataset->backgroundColor = $linecolors[$name]."55"; - $dataset->borderWidth=1; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - $dataset->stack = $name; - //$dataset->fill = "none"; - $dataset->yAxisID = 'y'; - $dataset->label = $name; - $dataset->data[] = $value; - $obj->datasets[] = clone $dataset; - } else { - $obj->labels[] = $value * 1000; - } - } - while ($row = $prodEst->fetch_assoc()) { - $ii = 1; - foreach ($row as $name => $value) { - if ($name != "time") { - $obj->datasets[$ii]->data[] = $value; - $ii++; - } else { - $obj->labels[] = $value * 1000; - } - } - } - } - - if($prodReal->num_rows > 1){ - $row = $prodReal->fetch_assoc(); - foreach ($row as $name => $value) { - $dataset = (object)[]; - if ($name != "time") { - $dataset->borderColor = $linecolors[$name]; - $dataset->backgroundColor = $linecolors[$name]."55"; - $dataset->borderWidth=1; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - $dataset->stack = $name; - //$dataset->fill = "none"; - $dataset->yAxisID = 'y'; - $dataset->label = $name; - $dataset->data[] = $value; - $obj->datasets[] = clone $dataset; - } - } - while ($row = $prodReal->fetch_assoc()) { - $ii = 2; - foreach ($row as $name => $value) { - if ($name != "time") { - $obj->datasets[$ii]->data[] = $value; - $ii++; - } - } - } - } - - - - -} - - -//header('Content-Type: application/json'); -echo json_encode($obj); -//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; += DATE(DATE_SUB(NOW(),INTERVAL 28 DAY)) And DATE(datetime) != DATE(NOW()) + GROUP BY WEEKDAY(datetime) +ORDER BY datetime"; + +$prodEstQuery = "SELECT UNIX_TIMESTAMP(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) AS 'time', + SUM(power*500) AS 'Vorhersage' + FROM simPower + WHERE DATE(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) >= DATE(DATE_SUB(NOW(),INTERVAL 7 DAY)) + GROUP BY DAY(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) + ORDER BY period_End;"; + +$prodRealQuery = "SELECT + UNIX_TIMESTAMP(datetime) AS 'time', + SUM(pvP/12) AS 'Tatsächliche Erzeugung', + SUM(-totalConsumption/12) AS 'Tatsächlicher Verbrauch' +FROM EnergyFlow +WHERE + DATE(datetime) >= DATE(DATE_SUB(NOW(),INTERVAL 7 DAY)) + GROUP BY DAY(datetime) +ORDER BY datetime"; + + +$linecolors["Tatsächliche Erzeugung"] = "#cccc00"; +$linecolors["Tatsächlicher Verbrauch"] = "#EE9900"; +$linecolors["Vorraussichtl. Verbrauch"] = "#BB4400"; +$linecolors["Auto UG"] = "#00aaFF"; +$linecolors["Auto OG"] = "#0044FF"; +$linecolors["Heizstab"] = "#FF0000"; +$linecolors["Batterieladung"] = "#00aa00"; +$linecolors["Einspeisung"] = "#b0b0b0"; +$linecolors["Ladestand"] = "#00aa00"; +$linecolors["Vorhersage"] = "#4444FF"; + + + +if (checkLogin()) { + $mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB); + $consEst = mysqli_query($mysql, $consEstQuery); + $prodEst = mysqli_query($mysql,$prodEstQuery); + $prodReal = mysqli_query($mysql,$prodRealQuery); + if(!$consEst){ + echo "Error:
".mysqli_error($mysql)."
"; + } + if(!$prodEst){ + echo "Error:
".mysqli_error($mysql)."
"; + } + if(!$prodRealQuery){ + echo "Error:
".mysqli_error($mysql)."
"; + } + + $obj = (object)[]; // Cast empty array to object + $obj->labels = []; + $obj->datasets = []; + $i = 0; + $filled = 0; + + if($consEst->num_rows > 1){ + $row = $consEst->fetch_assoc(); + foreach ($row as $name => $value) { + $dataset = (object)[]; + if ($name != "time") { + $dataset->borderColor = $linecolors[$name]; + $dataset->backgroundColor = $linecolors[$name]."66"; + $dataset->borderWidth=1; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + $dataset->stack = $name; + //$dataset->fill = "none"; + $dataset->yAxisID = 'y'; + $dataset->label = $name; + for($i=0;$i<7;$i++){ + $dataset->data[] = NULL; + } + $dataset->data[] = $value; + $obj->datasets[] = clone $dataset; + } + } + while ($row = $consEst->fetch_assoc()) { + $ii = 0; + foreach ($row as $name => $value) { + if ($name != "time") { + $obj->datasets[$ii]->data[] = $value; + $ii++; + } + } + } + } + + if ($prodEst->num_rows > 1) { + $row = $prodEst->fetch_assoc(); + foreach ($row as $name => $value) { + $dataset = (object)[]; + if ($name != "time") { + $dataset->borderColor = $linecolors[$name]; + $dataset->backgroundColor = $linecolors[$name]."55"; + $dataset->borderWidth=1; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + $dataset->stack = $name; + //$dataset->fill = "none"; + $dataset->yAxisID = 'y'; + $dataset->label = $name; + $dataset->data[] = $value; + $obj->datasets[] = clone $dataset; + } else { + $obj->labels[] = $value * 1000; + } + } + while ($row = $prodEst->fetch_assoc()) { + $ii = 1; + foreach ($row as $name => $value) { + if ($name != "time") { + $obj->datasets[$ii]->data[] = $value; + $ii++; + } else { + $obj->labels[] = $value * 1000; + } + } + } + } + + if($prodReal->num_rows > 1){ + $row = $prodReal->fetch_assoc(); + foreach ($row as $name => $value) { + $dataset = (object)[]; + if ($name != "time") { + $dataset->borderColor = $linecolors[$name]; + $dataset->backgroundColor = $linecolors[$name]."55"; + $dataset->borderWidth=1; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + $dataset->stack = $name; + //$dataset->fill = "none"; + $dataset->yAxisID = 'y'; + $dataset->label = $name; + $dataset->data[] = $value; + $obj->datasets[] = clone $dataset; + } + } + while ($row = $prodReal->fetch_assoc()) { + $ii = 2; + foreach ($row as $name => $value) { + if ($name != "time") { + $obj->datasets[$ii]->data[] = $value; + $ii++; + } + } + } + } + + + + +} + + +//header('Content-Type: application/json'); +echo json_encode($obj); +//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; diff --git a/ajax/getHeaterData.php b/ajax/getHeaterData.php index 763d235..a41a712 100644 --- a/ajax/getHeaterData.php +++ b/ajax/getHeaterData.php @@ -1,105 +1,105 @@ -".mysqli_error($mysql)."
"; - } - $obj = (object)[]; // Cast empty array to object - $obj->labels = []; - $obj->datasets = []; - $i = 0; - $filled = 0; - - if ($result->num_rows > 1) { - $ii = 0; - $row = $result->fetch_assoc(); - foreach ($row as $name => $value) { - $dataset = (object)[]; - if ($name != "time") { - $dataset->borderColor = $linecolors[$name]; - $dataset->backgroundColor = $linecolors[$name]."22"; - $dataset->borderWidth=2; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - if(strpos($name,"Speicher") === false) { - $dataset->fill = "none"; - } else { - // $dataset->stack = "Consumers"; - if ($filled == 0) { - $filled = 1; - $dataset->fill = "origin"; - } else { - $dataset->fill = "-1"; - } - $dataset->yAxisID = 'y'; - } - $dataset->label = $name; - - $dataset->data[] = $value; - $obj->datasets[] = clone $dataset; - $ii++; - } else { - $obj->labels[] = $value * 1000; - } - } - while ($row = $result->fetch_assoc()) { - $ii = 0; - foreach ($row as $name => $value) { - if ($name != "time") { - $obj->datasets[$ii]->data[] = $value; - $ii++; - } else { - $obj->labels[] = $value * 1000; - } - } - } - } - - -} - - -//header('Content-Type: application/json'); -echo json_encode($obj); -//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; +".mysqli_error($mysql)."
"; + } + $obj = (object)[]; // Cast empty array to object + $obj->labels = []; + $obj->datasets = []; + $i = 0; + $filled = 0; + + if ($result->num_rows > 1) { + $ii = 0; + $row = $result->fetch_assoc(); + foreach ($row as $name => $value) { + $dataset = (object)[]; + if ($name != "time") { + $dataset->borderColor = $linecolors[$name]; + $dataset->backgroundColor = $linecolors[$name]."22"; + $dataset->borderWidth=2; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + if(strpos($name,"Speicher") === false) { + $dataset->fill = "none"; + } else { + // $dataset->stack = "Consumers"; + if ($filled == 0) { + $filled = 1; + $dataset->fill = "origin"; + } else { + $dataset->fill = "-1"; + } + $dataset->yAxisID = 'y'; + } + $dataset->label = $name; + + $dataset->data[] = $value; + $obj->datasets[] = clone $dataset; + $ii++; + } else { + $obj->labels[] = $value * 1000; + } + } + while ($row = $result->fetch_assoc()) { + $ii = 0; + foreach ($row as $name => $value) { + if ($name != "time") { + $obj->datasets[$ii]->data[] = $value; + $ii++; + } else { + $obj->labels[] = $value * 1000; + } + } + } + } + + +} + + +//header('Content-Type: application/json'); +echo json_encode($obj); +//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; diff --git a/ajax/getProdData.php b/ajax/getProdData.php index d3b1f8a..5c85273 100644 --- a/ajax/getProdData.php +++ b/ajax/getProdData.php @@ -1,165 +1,165 @@ -0, gridP, 0) - IF(battP>0, battP, 0) > 0, -totalConsumption - IF(gridP>0, gridP, 0) - IF(battP>0, battP, 0), 0) AS Direktverbrauch, -IF(battP>0, battP, 0) AS Batteriebezug, -gridPcons AS Netzbezug, --totalConsumption AS Verbrauch,". -//IF(battP<0, -battP, 0) AS Batterieladung, -"soc AS Ladestand -FROM solarLog.EnergyFlow -WHERE EnergyFlow.datetime BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR) -ORDER BY EnergyFlow.datetime"; - -$simQuery = "SELECT -UNIX_TIMESTAMP(simPower.period_end) AS time, -power*1000 AS 'Vorhersage' -FROM solarLog.simPower -WHERE simPower.period_end BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR) -ORDER BY simPower.period_end"; -$linecolors["Solarleistung"] = "#FFFF00"; -$linecolors["Direktverbrauch"] = "#FFcc00"; -$linecolors["Verbrauch"] = "#FFaa44"; -$linecolors["Auto UG"] = "#00aaFF"; -$linecolors["Auto OG"] = "#0044FF"; -$linecolors["Netzbezug"] = "#FF0000"; -$linecolors["Batteriebezug"] = "#00aa00"; -$linecolors["Batterieladung"] = "#0033aa"; -$linecolors["Einspeisung"] = "#b0b0b0"; -$linecolors["Ladestand"] = "#00aa00"; -$linecolors["Vorhersage"] = "#2222FF"; - - - -if (checkLogin()) { - - $mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB); - $result = mysqli_query($mysql, $consQuery); - $simRes = mysqli_query($mysql,$simQuery); - if(!$result){ - echo "Error:
".mysqli_error($mysql)."
"; - } - $obj = (object)[]; // Cast empty array to object - $obj->labels = []; - $obj->datasets = []; - $i = 0; - $filled = 0; - if ($simRes->num_rows > 1) { - $dataset = (object)[]; - $row1 = $simRes->fetch_assoc(); - $dataset->borderColor = $linecolors["Vorhersage"]; - $dataset->backgroundColor = $linecolors["Vorhersage"]."55"; - $dataset->borderWidth=1.5; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - $dataset->stack = "sim"; - $dataset->fill = "none"; - $dataset->yAxisID = 'y'; - $dataset->label = "Vorhersage"; - /*$pt = (object)[]; - $pt->x = $row["time"]*1000; - $pt->y = $row["Vorhersage"]; - $dataset->data[] = clone $pt; - while ($row = $simRes->fetch_assoc()) { - $pt = (object)[]; - $pt->x = $row["time"]*1000 + 30*60*1000; - $pt->y = $row["Vorhersage"]; - $dataset->data[] = clone $pt; - }*/ - $obj->datasets[] = clone $dataset; - $rownext = $simRes->fetch_assoc(); - $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; - } - if ($result->num_rows > 1) { - $ii = 1; - $row = $result->fetch_assoc(); - foreach ($row as $name => $value) { - $dataset = (object)[]; - if ($name != "time") { - $dataset->borderColor = $linecolors[$name]; - $dataset->backgroundColor = $linecolors[$name]."22"; - $dataset->borderWidth=1; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - if ($name == "Solarleistung") { - $dataset->stack = "SolarPwr"; - $dataset->fill = "none"; - $dataset->yAxisID = 'y'; - }else if ($name == "Verbrauch") { - $dataset->stack = "ConsPwr"; - $dataset->fill = "1"; - $dataset->yAxisID = 'y'; - }else if ($name == "Batterieladung") { - $dataset->stack = "ConsPwr"; - $dataset->fill = "-1"; - $dataset->backgroundColor = $linecolors[$name]."77"; - $dataset->yAxisID = 'y'; - } else if ($name == "Ladestand") { - $dataset->stack = "Charge"; - $dataset->fill = "none"; - $dataset->yAxisID = 'y1'; - } else { - $dataset->stack = "Consumers"; - if ($filled == 0) { - $filled = 1; - $dataset->fill = "origin"; - } else { - $dataset->fill = "-1"; - } - $dataset->yAxisID = 'y'; - } - $dataset->label = $name; - - $dataset->data[] = $value; - $obj->datasets[] = clone $dataset; - $ii++; - } else { - $obj->labels[] = $value * 1000; - } - } - while ($row = $result->fetch_assoc()) { - $ii = 1; - foreach ($row as $name => $value) { - if ($name != "time") { - $obj->datasets[$ii]->data[] = $value; - $ii++; - } else { - if(($value * 1000) < $nextSimTimestamp){ - $obj->datasets[0]->data[] = $row1["Vorhersage"]; - }else{ - $row1 = $rownext; - $rownext = $simRes->fetch_assoc(); - $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; - $obj->datasets[0]->data[] = $row1["Vorhersage"]; - } - $obj->labels[] = $value * 1000; - } - } - } - } - $obj->labels[] = $nextSimTimestamp; //Draw future forecast - $obj->datasets[0]->data[] = $rownext["Vorhersage"]; - while($rownext = $simRes->fetch_assoc()){ - $obj->labels[] = $rownext["time"]*1000 + 30*60*1000; - $obj->datasets[0]->data[] = $rownext["Vorhersage"]; - } -} - - -//header('Content-Type: application/json'); -echo json_encode($obj); -//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; +0, gridP, 0) - IF(battP>0, battP, 0) > 0, -totalConsumption - IF(gridP>0, gridP, 0) - IF(battP>0, battP, 0), 0) AS Direktverbrauch, +IF(battP>0, battP, 0) AS Batteriebezug, +gridPcons AS Netzbezug, +-totalConsumption AS Verbrauch,". +//IF(battP<0, -battP, 0) AS Batterieladung, +"soc AS Ladestand +FROM solarLog.EnergyFlow +WHERE EnergyFlow.datetime BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR) +ORDER BY EnergyFlow.datetime"; + +$simQuery = "SELECT +UNIX_TIMESTAMP(simPower.period_end) AS time, +power*1000 AS 'Vorhersage' +FROM solarLog.simPower +WHERE simPower.period_end BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR) +ORDER BY simPower.period_end"; +$linecolors["Solarleistung"] = "#FFFF00"; +$linecolors["Direktverbrauch"] = "#FFcc00"; +$linecolors["Verbrauch"] = "#FFaa44"; +$linecolors["Auto UG"] = "#00aaFF"; +$linecolors["Auto OG"] = "#0044FF"; +$linecolors["Netzbezug"] = "#FF0000"; +$linecolors["Batteriebezug"] = "#00aa00"; +$linecolors["Batterieladung"] = "#0033aa"; +$linecolors["Einspeisung"] = "#b0b0b0"; +$linecolors["Ladestand"] = "#00aa00"; +$linecolors["Vorhersage"] = "#2222FF"; + + + +if (checkLogin()) { + + $mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB); + $result = mysqli_query($mysql, $consQuery); + $simRes = mysqli_query($mysql,$simQuery); + if(!$result){ + echo "Error:
".mysqli_error($mysql)."
"; + } + $obj = (object)[]; // Cast empty array to object + $obj->labels = []; + $obj->datasets = []; + $i = 0; + $filled = 0; + if ($simRes->num_rows > 1) { + $dataset = (object)[]; + $row1 = $simRes->fetch_assoc(); + $dataset->borderColor = $linecolors["Vorhersage"]; + $dataset->backgroundColor = $linecolors["Vorhersage"]."55"; + $dataset->borderWidth=1.5; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + $dataset->stack = "sim"; + $dataset->fill = "none"; + $dataset->yAxisID = 'y'; + $dataset->label = "Vorhersage"; + /*$pt = (object)[]; + $pt->x = $row["time"]*1000; + $pt->y = $row["Vorhersage"]; + $dataset->data[] = clone $pt; + while ($row = $simRes->fetch_assoc()) { + $pt = (object)[]; + $pt->x = $row["time"]*1000 + 30*60*1000; + $pt->y = $row["Vorhersage"]; + $dataset->data[] = clone $pt; + }*/ + $obj->datasets[] = clone $dataset; + $rownext = $simRes->fetch_assoc(); + $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; + } + if ($result->num_rows > 1) { + $ii = 1; + $row = $result->fetch_assoc(); + foreach ($row as $name => $value) { + $dataset = (object)[]; + if ($name != "time") { + $dataset->borderColor = $linecolors[$name]; + $dataset->backgroundColor = $linecolors[$name]."22"; + $dataset->borderWidth=1; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + if ($name == "Solarleistung") { + $dataset->stack = "SolarPwr"; + $dataset->fill = "none"; + $dataset->yAxisID = 'y'; + }else if ($name == "Verbrauch") { + $dataset->stack = "ConsPwr"; + $dataset->fill = "1"; + $dataset->yAxisID = 'y'; + }else if ($name == "Batterieladung") { + $dataset->stack = "ConsPwr"; + $dataset->fill = "-1"; + $dataset->backgroundColor = $linecolors[$name]."77"; + $dataset->yAxisID = 'y'; + } else if ($name == "Ladestand") { + $dataset->stack = "Charge"; + $dataset->fill = "none"; + $dataset->yAxisID = 'y1'; + } else { + $dataset->stack = "Consumers"; + if ($filled == 0) { + $filled = 1; + $dataset->fill = "origin"; + } else { + $dataset->fill = "-1"; + } + $dataset->yAxisID = 'y'; + } + $dataset->label = $name; + + $dataset->data[] = $value; + $obj->datasets[] = clone $dataset; + $ii++; + } else { + $obj->labels[] = $value * 1000; + } + } + while ($row = $result->fetch_assoc()) { + $ii = 1; + foreach ($row as $name => $value) { + if ($name != "time") { + $obj->datasets[$ii]->data[] = $value; + $ii++; + } else { + if(($value * 1000) < $nextSimTimestamp){ + $obj->datasets[0]->data[] = $row1["Vorhersage"]; + }else{ + $row1 = $rownext; + $rownext = $simRes->fetch_assoc(); + $nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000; + $obj->datasets[0]->data[] = $row1["Vorhersage"]; + } + $obj->labels[] = $value * 1000; + } + } + } + } + $obj->labels[] = $nextSimTimestamp; //Draw future forecast + $obj->datasets[0]->data[] = $rownext["Vorhersage"]; + while($rownext = $simRes->fetch_assoc()){ + $obj->labels[] = $rownext["time"]*1000 + 30*60*1000; + $obj->datasets[0]->data[] = $rownext["Vorhersage"]; + } +} + + +//header('Content-Type: application/json'); +echo json_encode($obj); +//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; ?> \ No newline at end of file diff --git a/ajax/getStats.php b/ajax/getStats.php index c146b5e..8dc5104 100644 --- a/ajax/getStats.php +++ b/ajax/getStats.php @@ -1,211 +1,211 @@ - date("Y")){ - exit; -} -if(isset($_GET["type"])){ - if(strtolower($_GET["type"]) =="lastyear"){ - $_GET["year"] = date("Y")-1; - }elseif(strtolower($_GET["type"]) =="prelastyear"){ - $_GET["year"] = date("Y")-2; - } -} - -/** - * Die Jahresstatistik als eine Abfrage - jetzt auf stats_daily. - * - * Gerechnet wird nicht mehr ueber die Rohdaten aus EnergyFlow, sondern ueber - * die Tageswerte. Zwei Gruende: - * - * 1. Die Rohdaten werden nach zwoelf Monaten ausgeduennt bzw. ins - * Stundenarchiv verschoben (solarlog-rollup.sh). Eine Kachel, die auf - * EnergyFlow rechnet, zeigt fuer alte Jahre danach zu wenig an. - * 2. Ein Jahr sind 365 Zeilen statt rund 105.000. - * - * Die Tarife bleiben unveraendert: je Zeile ein Stichtag, das Ende holt - * LEAD() beim naechsten. Weil ein Stichtag ein DATUM ist, kann ein Tag nie - * zwei Tarife haben - die Verdichtung auf Tage verliert hier also nichts. - * - * Was in stats_daily vorberechnet sein MUSS, weil es eine Bedingung je - * Messwert auswertet und sich aus Tagessummen nicht rekonstruieren laesst: - * - * eigenverbrauch_kwh pv - Einspeisung - Batterieladung - * - Heizstab + Batterieentladung. Dazu addiert - * wird pv_fehlbetrag_kwh: bei Ausfall einzelner - * Wechselrichter fehlt deren Beitrag in pv, alle - * Senken sind dagegen gemessen - die Differenz - * faellt sonst zu klein oder negativ aus. - * heizstab_solar_kwh Heizstab, anteilig nach autonomy - * auto_*_solar_kwh Wallbox, anteilig nach autonomy - dem vom - * Geraet in feiner Aufloesung gerechneten - * Nicht-Netz-Anteil der Zufuhr - * - * Einheiten sind in stats_daily bereits bereinigt: PL*_EV und PL*_EVog - * liefern Kilowatt, alle uebrigen Leistungsspalten Watt. In der alten - * Abfrage stand das als /12 gegen /12000 nebeneinander. - * - * Die Solaranteile teilen jetzt durch NULLIF(...): ohne Ladung im Jahr kam - * vorher 0/0 heraus. - * - * "Ersparnis Batterie" rechnet den Nutzen netto: was die entladene Energie - * aus dem Netz gekostet haette, abzueglich der Verguetung, die fuer den zum - * Laden verwendeten Strom entgangen ist. Brutto waere die Kachel zu - * freundlich - der Speicher ist nicht umsonst zu fuellen. - * - * Achtung beim Lesen: "Verbrauchsersparnis" ist der GESAMTE Eigenverbrauch - * zum Strompreis. "Ersparnis Batterie" und "Ersparnis Solarladung" sind - * Teilmengen davon, nach Herkunft bzw. Verwendung herausgegriffen. Sie - * duerfen nicht addiert werden. - * - * Der Grundpreis fehlt weiterhin bewusst - er haengt an den Tagen des - * Jahres, nicht an einem Messwert, und kommt aus grundpreisImJahr() dazu. - */ -function statistikAbfrage($jahr) -{ - return "WITH - preis AS (SELECT active_date, cost, gain, - LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM gridCosts), - gas AS (SELECT active_date, cost, kwhPerLitre, - LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM gasCosts), - sprit AS (SELECT active_date, cost, lPer100km, kwhPer100km, - LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM fuelCosts) -SELECT - SUM(d.netz_bezug_kwh) AS 'Stromverbrauch', - SUM(d.netz_bezug_kwh * preis.cost) AS 'Stromkosten', - SUM((d.eigenverbrauch_kwh + IFNULL(d.pv_fehlbetrag_kwh,0)) * preis.cost) AS 'Verbrauchsersparnis', - SUM(d.netz_einsp_kwh * preis.gain) AS 'Einspeisevergütung', - SUM(d.heizstab_solar_kwh * gas.cost / NULLIF(gas.kwhPerLitre,0)) AS 'Ersparnis Heizung', - SUM(d.batt_entladen_kwh * preis.cost - d.batt_laden_kwh * preis.gain) AS 'Ersparnis Batterie', - SUM(d.autarkie_avg * d.stunden_erfasst) / NULLIF(SUM(d.stunden_erfasst),0) AS 'Ø Autarkie', - SUM(d.auto_eg_kwh) AS 'Autoladung ges. EG', - SUM(d.auto_eg_solar_kwh) / NULLIF(SUM(d.auto_eg_kwh),0) * 100 AS 'Autoladung Solar EG', - SUM(d.auto_eg_solar_kwh * preis.cost) AS 'Ersparnis Solarladung EG', - SUM(d.auto_eg_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0)) AS 'Benzin gespart EG', - SUM(d.auto_eg_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0) * sprit.cost) AS 'Ersparnis Benzin EG', - SUM(d.auto_og_kwh) AS 'Autoladung ges. OG', - SUM(d.auto_og_solar_kwh) / NULLIF(SUM(d.auto_og_kwh),0) * 100 AS 'Autoladung Solar OG', - SUM(d.auto_og_solar_kwh * preis.cost) AS 'Ersparnis Solarladung OG', - SUM(d.auto_og_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0)) AS 'Benzin gespart OG', - SUM(d.auto_og_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0) * sprit.cost) AS 'Ersparnis Benzin OG' -FROM stats_daily d - JOIN preis ON d.tag >= preis.active_date AND (preis.folgt IS NULL OR d.tag < preis.folgt) - LEFT JOIN gas ON d.tag >= gas.active_date AND (gas.folgt IS NULL OR d.tag < gas.folgt) - LEFT JOIN sprit ON d.tag >= sprit.active_date AND (sprit.folgt IS NULL OR d.tag < sprit.folgt) -WHERE YEAR(d.tag) = " . intval($jahr) . ";"; -} - -$Query = statistikAbfrage($_GET["year"]); -$PrevQuery = statistikAbfrage($_GET["year"] - 1); - -// Reihenfolge wie in der Abfrage, mit "Verg.+Einsp. Strom" an Stelle 4. -$units = Array("kWh","€","€","€","€","€","€","%", - "kWh","%","€","L","€", - "kWh","%","€","L","€"); -// Nur bei Verbrauch und Kosten ist weniger besser. -$LessIsBetter = array_merge([true, true], array_fill(0, count($units) - 2, false)); - -function array_insert($array,$values,$offset) { - return array_slice($array, 0, $offset, true) + $values + array_slice($array, $offset, NULL, true); -} - - -if (checkLogin()) { - - $html = "
"; - $mysql = solarDb(); - $Res = mysqli_query($mysql,$Query); - $ResPrev = mysqli_query($mysql,$PrevQuery); - if(!$Res || !$ResPrev){ - echo "Error:
".mysqli_error($mysql)."
"; - } - if ($Res->num_rows > 0) { - $i = 0; - $row = $Res->fetch_assoc(); - $rowPrev = $ResPrev->fetch_assoc(); - // Der Grundpreis faellt unabhaengig vom Verbrauch an und laesst sich - // deshalb nicht aus den Messwerten summieren. - $row["Stromkosten"] += grundpreisImJahr($_GET["year"]); - $rowPrev["Stromkosten"] += grundpreisImJahr($_GET["year"] - 1); - //$row["Vergütung+Einsparung Strom"] = $row["Einspeisevergütung"]+$row["Verbrauchsersparnis"]; - $row = array_insert($row,["Verg.+Einsp. Strom" => $row["Einspeisevergütung"]+$row["Verbrauchsersparnis"]],4); - $rowPrev = array_insert($rowPrev,["Verg.+Einsp. Strom" => $rowPrev["Einspeisevergütung"]+$rowPrev["Verbrauchsersparnis"]],4); - $i = 0; - foreach ($row as $name => $value) { - if(str_starts_with($name,"Autoladung ges.")){ - $html .= "

"; - $html .= "
"; - } - /*$html .= "
-
-
- ".$name." - ". - number_format(floatval($value),2,",",".") - ."".$units[$i++]." - -
- -
-
";*/ - $html .= "
-
-
- ".$name." -
- - -
-
"; - $i++; - } - } - $html .= "
"; -} - - -//header('Content-Type: application/json'); -echo $html; -//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; + date("Y")){ + exit; +} +if(isset($_GET["type"])){ + if(strtolower($_GET["type"]) =="lastyear"){ + $_GET["year"] = date("Y")-1; + }elseif(strtolower($_GET["type"]) =="prelastyear"){ + $_GET["year"] = date("Y")-2; + } +} + +/** + * Die Jahresstatistik als eine Abfrage - jetzt auf stats_daily. + * + * Gerechnet wird nicht mehr ueber die Rohdaten aus EnergyFlow, sondern ueber + * die Tageswerte. Zwei Gruende: + * + * 1. Die Rohdaten werden nach zwoelf Monaten ausgeduennt bzw. ins + * Stundenarchiv verschoben (solarlog-rollup.sh). Eine Kachel, die auf + * EnergyFlow rechnet, zeigt fuer alte Jahre danach zu wenig an. + * 2. Ein Jahr sind 365 Zeilen statt rund 105.000. + * + * Die Tarife bleiben unveraendert: je Zeile ein Stichtag, das Ende holt + * LEAD() beim naechsten. Weil ein Stichtag ein DATUM ist, kann ein Tag nie + * zwei Tarife haben - die Verdichtung auf Tage verliert hier also nichts. + * + * Was in stats_daily vorberechnet sein MUSS, weil es eine Bedingung je + * Messwert auswertet und sich aus Tagessummen nicht rekonstruieren laesst: + * + * eigenverbrauch_kwh pv - Einspeisung - Batterieladung + * - Heizstab + Batterieentladung. Dazu addiert + * wird pv_fehlbetrag_kwh: bei Ausfall einzelner + * Wechselrichter fehlt deren Beitrag in pv, alle + * Senken sind dagegen gemessen - die Differenz + * faellt sonst zu klein oder negativ aus. + * heizstab_solar_kwh Heizstab, anteilig nach autonomy + * auto_*_solar_kwh Wallbox, anteilig nach autonomy - dem vom + * Geraet in feiner Aufloesung gerechneten + * Nicht-Netz-Anteil der Zufuhr + * + * Einheiten sind in stats_daily bereits bereinigt: PL*_EV und PL*_EVog + * liefern Kilowatt, alle uebrigen Leistungsspalten Watt. In der alten + * Abfrage stand das als /12 gegen /12000 nebeneinander. + * + * Die Solaranteile teilen jetzt durch NULLIF(...): ohne Ladung im Jahr kam + * vorher 0/0 heraus. + * + * "Ersparnis Batterie" rechnet den Nutzen netto: was die entladene Energie + * aus dem Netz gekostet haette, abzueglich der Verguetung, die fuer den zum + * Laden verwendeten Strom entgangen ist. Brutto waere die Kachel zu + * freundlich - der Speicher ist nicht umsonst zu fuellen. + * + * Achtung beim Lesen: "Verbrauchsersparnis" ist der GESAMTE Eigenverbrauch + * zum Strompreis. "Ersparnis Batterie" und "Ersparnis Solarladung" sind + * Teilmengen davon, nach Herkunft bzw. Verwendung herausgegriffen. Sie + * duerfen nicht addiert werden. + * + * Der Grundpreis fehlt weiterhin bewusst - er haengt an den Tagen des + * Jahres, nicht an einem Messwert, und kommt aus grundpreisImJahr() dazu. + */ +function statistikAbfrage($jahr) +{ + return "WITH + preis AS (SELECT active_date, cost, gain, + LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM gridCosts), + gas AS (SELECT active_date, cost, kwhPerLitre, + LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM gasCosts), + sprit AS (SELECT active_date, cost, lPer100km, kwhPer100km, + LEAD(active_date) OVER (ORDER BY active_date) AS folgt FROM fuelCosts) +SELECT + SUM(d.netz_bezug_kwh) AS 'Stromverbrauch', + SUM(d.netz_bezug_kwh * preis.cost) AS 'Stromkosten', + SUM((d.eigenverbrauch_kwh + IFNULL(d.pv_fehlbetrag_kwh,0)) * preis.cost) AS 'Verbrauchsersparnis', + SUM(d.netz_einsp_kwh * preis.gain) AS 'Einspeisevergütung', + SUM(d.heizstab_solar_kwh * gas.cost / NULLIF(gas.kwhPerLitre,0)) AS 'Ersparnis Heizung', + SUM(d.batt_entladen_kwh * preis.cost - d.batt_laden_kwh * preis.gain) AS 'Ersparnis Batterie', + SUM(d.autarkie_avg * d.stunden_erfasst) / NULLIF(SUM(d.stunden_erfasst),0) AS 'Ø Autarkie', + SUM(d.auto_eg_kwh) AS 'Autoladung ges. EG', + SUM(d.auto_eg_solar_kwh) / NULLIF(SUM(d.auto_eg_kwh),0) * 100 AS 'Autoladung Solar EG', + SUM(d.auto_eg_solar_kwh * preis.cost) AS 'Ersparnis Solarladung EG', + SUM(d.auto_eg_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0)) AS 'Benzin gespart EG', + SUM(d.auto_eg_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0) * sprit.cost) AS 'Ersparnis Benzin EG', + SUM(d.auto_og_kwh) AS 'Autoladung ges. OG', + SUM(d.auto_og_solar_kwh) / NULLIF(SUM(d.auto_og_kwh),0) * 100 AS 'Autoladung Solar OG', + SUM(d.auto_og_solar_kwh * preis.cost) AS 'Ersparnis Solarladung OG', + SUM(d.auto_og_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0)) AS 'Benzin gespart OG', + SUM(d.auto_og_kwh * sprit.lPer100km / NULLIF(sprit.kwhPer100km,0) * sprit.cost) AS 'Ersparnis Benzin OG' +FROM stats_daily d + JOIN preis ON d.tag >= preis.active_date AND (preis.folgt IS NULL OR d.tag < preis.folgt) + LEFT JOIN gas ON d.tag >= gas.active_date AND (gas.folgt IS NULL OR d.tag < gas.folgt) + LEFT JOIN sprit ON d.tag >= sprit.active_date AND (sprit.folgt IS NULL OR d.tag < sprit.folgt) +WHERE YEAR(d.tag) = " . intval($jahr) . ";"; +} + +$Query = statistikAbfrage($_GET["year"]); +$PrevQuery = statistikAbfrage($_GET["year"] - 1); + +// Reihenfolge wie in der Abfrage, mit "Verg.+Einsp. Strom" an Stelle 4. +$units = Array("kWh","€","€","€","€","€","€","%", + "kWh","%","€","L","€", + "kWh","%","€","L","€"); +// Nur bei Verbrauch und Kosten ist weniger besser. +$LessIsBetter = array_merge([true, true], array_fill(0, count($units) - 2, false)); + +function array_insert($array,$values,$offset) { + return array_slice($array, 0, $offset, true) + $values + array_slice($array, $offset, NULL, true); +} + + +if (checkLogin()) { + + $html = "
"; + $mysql = solarDb(); + $Res = mysqli_query($mysql,$Query); + $ResPrev = mysqli_query($mysql,$PrevQuery); + if(!$Res || !$ResPrev){ + echo "Error:
".mysqli_error($mysql)."
"; + } + if ($Res->num_rows > 0) { + $i = 0; + $row = $Res->fetch_assoc(); + $rowPrev = $ResPrev->fetch_assoc(); + // Der Grundpreis faellt unabhaengig vom Verbrauch an und laesst sich + // deshalb nicht aus den Messwerten summieren. + $row["Stromkosten"] += grundpreisImJahr($_GET["year"]); + $rowPrev["Stromkosten"] += grundpreisImJahr($_GET["year"] - 1); + //$row["Vergütung+Einsparung Strom"] = $row["Einspeisevergütung"]+$row["Verbrauchsersparnis"]; + $row = array_insert($row,["Verg.+Einsp. Strom" => $row["Einspeisevergütung"]+$row["Verbrauchsersparnis"]],4); + $rowPrev = array_insert($rowPrev,["Verg.+Einsp. Strom" => $rowPrev["Einspeisevergütung"]+$rowPrev["Verbrauchsersparnis"]],4); + $i = 0; + foreach ($row as $name => $value) { + if(str_starts_with($name,"Autoladung ges.")){ + $html .= "

"; + $html .= "
"; + } + /*$html .= "
+
+
+ ".$name." + ". + number_format(floatval($value),2,",",".") + ."".$units[$i++]." + +
+ +
+
";*/ + $html .= "
+
+
+ ".$name." +
+ + +
+
"; + $i++; + } + } + $html .= "
"; +} + + +//header('Content-Type: application/json'); +echo $html; +//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; diff --git a/ajax/getSunrise.php b/ajax/getSunrise.php index 956d6b4..d741f03 100644 --- a/ajax/getSunrise.php +++ b/ajax/getSunrise.php @@ -1,65 +1,65 @@ -" . mysqli_error($mysql) . "
"; - } - //$obj[] = (object)[]; // Cast empty array to object - - while ($row = $result->fetch_assoc()) { - $ii = 1; - $anno = (object)[]; - if($row["sunrise"] < (time()+$_GET["FROM"]*60*60)){ - $anno->xMin = (time()+$_GET["FROM"]*60*60)*1000; - }else{ - $anno->xMin = $row["sunrise"]*1000; - } - if($row["sunset"] > (time()+$_GET["TO"]*60*60)){ - $anno->xMax = round(time()+$_GET["TO"]*60*60)*1000; - }else{ - $anno->xMax = $row["sunset"]*1000; - } - $anno->borderWidth = 0; - if($row["sunset"] > (time()+$_GET["FROM"]*60*60)){ - $obj[] = clone $anno; - } - - } -} - - -//header('Content-Type: application/json'); -echo json_encode($obj); -//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; +" . mysqli_error($mysql) . "
"; + } + //$obj[] = (object)[]; // Cast empty array to object + + while ($row = $result->fetch_assoc()) { + $ii = 1; + $anno = (object)[]; + if($row["sunrise"] < (time()+$_GET["FROM"]*60*60)){ + $anno->xMin = (time()+$_GET["FROM"]*60*60)*1000; + }else{ + $anno->xMin = $row["sunrise"]*1000; + } + if($row["sunset"] > (time()+$_GET["TO"]*60*60)){ + $anno->xMax = round(time()+$_GET["TO"]*60*60)*1000; + }else{ + $anno->xMax = $row["sunset"]*1000; + } + $anno->borderWidth = 0; + if($row["sunset"] > (time()+$_GET["FROM"]*60*60)){ + $obj[] = clone $anno; + } + + } +} + + +//header('Content-Type: application/json'); +echo json_encode($obj); +//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; diff --git a/ajax/getWaterData.php b/ajax/getWaterData.php index 1c20a1f..44fc905 100644 --- a/ajax/getWaterData.php +++ b/ajax/getWaterData.php @@ -1,65 +1,65 @@ -".mysqli_error($mysql)."
"; - } - $obj = (object)[]; // Cast empty array to object - $obj->labels = []; - $obj->datasets = []; - $i = 0; - $filled = 0; - if ($waterRes->num_rows > 1) { - $dataset = (object)[]; - $row = $waterRes->fetch_assoc(); - $dataset->borderColor = $linecolors["Wasserverbrauch"]; - $dataset->backgroundColor = $linecolors["Wasserverbrauch"]."00"; - $dataset->borderWidth=2; - $dataset->pointRadius= 0; - $dataset->pointHoverRadius= 5; - $dataset->tension=0.2; - $dataset->fill = "none"; - $dataset->yAxisID = 'y1'; - $dataset->label = "Wasserverbrauch"; - $pt = (object)[]; - $pt->x = $row["time"]*1000; - $pt->y = $row["Wasserverbrauch"]; - $dataset->data[] = clone $pt; - while ($row = $waterRes->fetch_assoc()) { - $pt = (object)[]; - $pt->x = $row["time"]*1000; - $pt->y = $row["Wasserverbrauch"]; - $dataset->data[] = clone $pt; - } - $obj->datasets[] = clone $dataset; - } - - -} - - -//header('Content-Type: application/json'); -echo json_encode($obj); -//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; +".mysqli_error($mysql)."
"; + } + $obj = (object)[]; // Cast empty array to object + $obj->labels = []; + $obj->datasets = []; + $i = 0; + $filled = 0; + if ($waterRes->num_rows > 1) { + $dataset = (object)[]; + $row = $waterRes->fetch_assoc(); + $dataset->borderColor = $linecolors["Wasserverbrauch"]; + $dataset->backgroundColor = $linecolors["Wasserverbrauch"]."00"; + $dataset->borderWidth=2; + $dataset->pointRadius= 0; + $dataset->pointHoverRadius= 5; + $dataset->tension=0.2; + $dataset->fill = "none"; + $dataset->yAxisID = 'y1'; + $dataset->label = "Wasserverbrauch"; + $pt = (object)[]; + $pt->x = $row["time"]*1000; + $pt->y = $row["Wasserverbrauch"]; + $dataset->data[] = clone $pt; + while ($row = $waterRes->fetch_assoc()) { + $pt = (object)[]; + $pt->x = $row["time"]*1000; + $pt->y = $row["Wasserverbrauch"]; + $dataset->data[] = clone $pt; + } + $obj->datasets[] = clone $dataset; + } + + +} + + +//header('Content-Type: application/json'); +echo json_encode($obj); +//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}'; diff --git a/ajax/heater.php b/ajax/heater.php index c060248..ad14354 100644 --- a/ajax/heater.php +++ b/ajax/heater.php @@ -1,57 +1,57 @@ - 0 && $_POST["setMode"] < 60){ - $pwr = $_POST["setMode"]*100; - $headers[] = "GET /set?mode=man&pwr=".$pwr." HTTP/1.1"; - $headers[] = "Host: ".$hostname; - $headers[] = ""; - $remote = fsockopen("tcp://".$hostname, 80, $errno, $errstr, 5); - fwrite($remote, implode("\r\n", $headers)."\r\n"); - $file = ''; - $file .= fread($remote, 1024); - fclose($remote); - }elseif($_POST["setMode"] == 0){ - $headers[] = "GET /set?mode=eco&pwr=0 HTTP/1.1"; - $headers[] = "Host: ".$hostname; - $headers[] = ""; - $remote = fsockopen("tcp://".$hostname, 80, $errno, $errstr, 5); - fwrite($remote, implode("\r\n", $headers)."\r\n"); - $file = ''; - $file .= fread($remote, 1024); - fclose($remote); - } - $close = 1; - - } - } else { - $close = 1; -} -if (!$close) { - echo << -
-
-
- - - -
- Auto - 6.0 kW -
-
-
-
-ENDE; -} + 0 && $_POST["setMode"] < 60){ + $pwr = $_POST["setMode"]*100; + $headers[] = "GET /set?mode=man&pwr=".$pwr." HTTP/1.1"; + $headers[] = "Host: ".$hostname; + $headers[] = ""; + $remote = fsockopen("tcp://".$hostname, 80, $errno, $errstr, 5); + fwrite($remote, implode("\r\n", $headers)."\r\n"); + $file = ''; + $file .= fread($remote, 1024); + fclose($remote); + }elseif($_POST["setMode"] == 0){ + $headers[] = "GET /set?mode=eco&pwr=0 HTTP/1.1"; + $headers[] = "Host: ".$hostname; + $headers[] = ""; + $remote = fsockopen("tcp://".$hostname, 80, $errno, $errstr, 5); + fwrite($remote, implode("\r\n", $headers)."\r\n"); + $file = ''; + $file .= fread($remote, 1024); + fclose($remote); + } + $close = 1; + + } + } else { + $close = 1; +} +if (!$close) { + echo << +
+
+
+ + + +
+ Auto + 6.0 kW +
+
+
+
+ENDE; +} diff --git a/ajax/phpMQTT.php b/ajax/phpMQTT.php index 0df8003..c028b0d 100644 --- a/ajax/phpMQTT.php +++ b/ajax/phpMQTT.php @@ -1,671 +1,671 @@ - 'CONNECT', - 2 => 'CONNACK', - 3 => 'PUBLISH', - 4 => 'PUBACK', - 5 => 'PUBREC', - 6 => 'PUBREL', - 7 => 'PUBCOMP', - 8 => 'SUBSCRIBE', - 9 => 'SUBACK', - 10 => 'UNSUBSCRIBE', - 11 => 'UNSUBACK', - 12 => 'PINGREQ', - 13 => 'PINGRESP', - 14 => 'DISCONNECT' - ]; - - /** - * phpMQTT constructor. - * - * @param $address - * @param $port - * @param $clientid - * @param null $cafile - */ - public function __construct($address, $port, $clientid, $cafile = null) - { - $this->broker($address, $port, $clientid, $cafile); - } - - /** - * Sets the broker details - * - * @param $address - * @param $port - * @param $clientid - * @param null $cafile - */ - public function broker($address, $port, $clientid, $cafile = null): void - { - $this->address = $address; - $this->port = $port; - $this->clientid = $clientid; - $this->cafile = $cafile; - } - - /** - * Will try and connect, if fails it will sleep 10s and try again, this will enable the script to recover from a network outage - * - * @param bool $clean - should the client send a clean session flag - * @param null $will - * @param null $username - * @param null $password - * - * @return bool - */ - public function connect_auto($clean = true, $will = null, $username = null, $password = null): bool - { - while ($this->connect($clean, $will, $username, $password) === false) { - sleep(10); - } - return true; - } - - /** - * @param bool $clean - should the client send a clean session flag - * @param null $will - * @param null $username - * @param null $password - * - * @return bool - */ - public function connect($clean = true, $will = null, $username = null, $password = null): bool - { - if ($will) { - $this->will = $will; - } - if ($username) { - $this->username = $username; - } - if ($password) { - $this->password = $password; - } - - if ($this->cafile) { - $socketContext = stream_context_create( - [ - 'ssl' => [ - 'verify_peer_name' => true, - 'cafile' => $this->cafile - ] - ] - ); - $this->socket = stream_socket_client('tls://' . $this->address . ':' . $this->port, $errno, $errstr, 60, STREAM_CLIENT_CONNECT, $socketContext); - } else { - $this->socket = stream_socket_client('tcp://' . $this->address . ':' . $this->port, $errno, $errstr, 60, STREAM_CLIENT_CONNECT); - } - - if (!$this->socket) { - $this->_errorMessage("stream_socket_create() $errno, $errstr"); - return false; - } - - stream_set_timeout($this->socket, 5); - stream_set_blocking($this->socket, 0); - - $i = 0; - $buffer = ''; - - $buffer .= chr(0x00); - $i++; // Length MSB - $buffer .= chr(0x04); - $i++; // Length LSB - $buffer .= chr(0x4d); - $i++; // M - $buffer .= chr(0x51); - $i++; // Q - $buffer .= chr(0x54); - $i++; // T - $buffer .= chr(0x54); - $i++; // T - $buffer .= chr(0x04); - $i++; // // Protocol Level - - //No Will - $var = 0; - if ($clean) { - $var += 2; - } - - //Add will info to header - if ($this->will !== null) { - $var += 4; // Set will flag - $var += ($this->will['qos'] << 3); //Set will qos - if ($this->will['retain']) { - $var += 32; - } //Set will retain - } - - if ($this->username !== null) { - $var += 128; - } //Add username to header - if ($this->password !== null) { - $var += 64; - } //Add password to header - - $buffer .= chr($var); - $i++; - - //Keep alive - $buffer .= chr($this->keepalive >> 8); - $i++; - $buffer .= chr($this->keepalive & 0xff); - $i++; - - $buffer .= $this->strwritestring($this->clientid, $i); - - //Adding will to payload - if ($this->will !== null) { - $buffer .= $this->strwritestring($this->will['topic'], $i); - $buffer .= $this->strwritestring($this->will['content'], $i); - } - - if ($this->username !== null) { - $buffer .= $this->strwritestring($this->username, $i); - } - if ($this->password !== null) { - $buffer .= $this->strwritestring($this->password, $i); - } - - $head = chr(0x10); - - while ($i > 0) { - $encodedByte = $i % 128; - $i /= 128; - $i = (int)$i; - if ($i > 0) { - $encodedByte |= 128; - } - $head .= chr($encodedByte); - } - - fwrite($this->socket, $head, 2); - fwrite($this->socket, $buffer); - - $string = $this->read(4); - - if (ord($string[0]) >> 4 === 2 && $string[3] === chr(0)) { - $this->_debugMessage('Connected to Broker'); - } else { - $this->_errorMessage( - sprintf( - "Connection failed! (Error: 0x%02x 0x%02x)\n", - ord($string[0]), - ord($string[3]) - ) - ); - return false; - } - - $this->timesinceping = time(); - - return true; - } - - /** - * Reads in so many bytes - * - * @param int $int - * @param bool $nb - * - * @return false|string - */ - public function read($int = 8192, $nb = false) - { - $string = ''; - $togo = $int; - - if ($nb) { - return fread($this->socket, $togo); - } - - while (!feof($this->socket) && $togo > 0) { - $fread = fread($this->socket, $togo); - $string .= $fread; - $togo = $int - strlen($string); - } - - return $string; - } - - /** - * Subscribes to a topic, wait for message and return it - * - * @param $topic - * @param $qos - * - * @return string - */ - public function subscribeAndWaitForMessage($topic, $qos): string - { - $this->subscribe( - [ - $topic => [ - 'qos' => $qos, - 'function' => '__direct_return_message__' - ] - ] - ); - - do { - $return = $this->proc(); - } while ($return === true); - - return $return; - } - - /** - * subscribes to topics - * - * @param $topics - * @param int $qos - */ - public function subscribe($topics, $qos = 0): void - { - $i = 0; - $buffer = ''; - $id = $this->msgid; - $buffer .= chr($id >> 8); - $i++; - $buffer .= chr($id % 256); - $i++; - - foreach ($topics as $key => $topic) { - $buffer .= $this->strwritestring($key, $i); - $buffer .= chr($topic['qos']); - $i++; - $this->topics[$key] = $topic; - } - - $cmd = 0x82; - //$qos - $cmd += ($qos << 1); - - $head = chr($cmd); - $head .= $this->setmsglength($i); - fwrite($this->socket, $head, strlen($head)); - - $this->_fwrite($buffer); - $string = $this->read(2); - - $bytes = ord(substr($string, 1, 1)); - $this->read($bytes); - } - - /** - * Sends a keep alive ping - */ - public function ping(): void - { - $head = chr(0xc0); - $head .= chr(0x00); - fwrite($this->socket, $head, 2); - $this->timesinceping = time(); - $this->_debugMessage('ping sent'); - } - - /** - * sends a proper disconnect cmd - */ - public function disconnect(): void - { - $head = ' '; - $head[0] = chr(0xe0); - $head[1] = chr(0x00); - fwrite($this->socket, $head, 2); - } - - /** - * Sends a proper disconnect, then closes the socket - */ - public function close(): void - { - $this->disconnect(); - stream_socket_shutdown($this->socket, STREAM_SHUT_WR); - } - - /** - * Publishes $content on a $topic - * - * @param $topic - * @param $content - * @param int $qos - * @param bool $retain - */ - public function publish($topic, $content, $qos = 0, $retain = false): void - { - $i = 0; - $buffer = ''; - - $buffer .= $this->strwritestring($topic, $i); - - if ($qos) { - $id = $this->msgid++; - $buffer .= chr($id >> 8); - $i++; - $buffer .= chr($id % 256); - $i++; - } - - $buffer .= $content; - $i += strlen($content); - - $head = ' '; - $cmd = 0x30; - if ($qos) { - $cmd += $qos << 1; - } - if (empty($retain) === false) { - ++$cmd; - } - - $head[0] = chr($cmd); - $head .= $this->setmsglength($i); - - fwrite($this->socket, $head, strlen($head)); - $this->_fwrite($buffer); - } - - /** - * Writes a string to the socket - * - * @param $buffer - * - * @return bool|int - */ - protected function _fwrite($buffer) - { - $buffer_length = strlen($buffer); - for ($written = 0; $written < $buffer_length; $written += $fwrite) { - $fwrite = fwrite($this->socket, substr($buffer, $written)); - if ($fwrite === false) { - return false; - } - } - return $buffer_length; - } - - /** - * Processes a received topic - * - * @param $msg - * - * @retrun bool|string - */ - public function message($msg) - { - $tlen = (ord($msg[0]) << 8) + ord($msg[1]); - $topic = substr($msg, 2, $tlen); - $msg = substr($msg, ($tlen + 2)); - $found = false; - foreach ($this->topics as $key => $top) { - if (preg_match( - '/^' . str_replace( - '#', - '.*', - str_replace( - '+', - "[^\/]*", - str_replace( - '/', - "\/", - str_replace( - '$', - '\$', - $key - ) - ) - ) - ) . '$/', - $topic - )) { - $found = true; - - if ($top['function'] === '__direct_return_message__') { - return $msg; - } - - if (is_callable($top['function'])) { - call_user_func($top['function'], $topic, $msg); - } else { - $this->_errorMessage('Message received on topic ' . $topic . ' but function is not callable.'); - } - } - } - - if ($found === false) { - $this->_debugMessage('msg received but no match in subscriptions'); - } - - return $found; - } - - /** - * The processing loop for an "always on" client - * set true when you are doing other stuff in the loop good for - * watching something else at the same time - * - * @param bool $loop - * - * @return bool | string - */ - public function proc(bool $loop = true) - { - if (feof($this->socket)) { - $this->_debugMessage('eof receive going to reconnect for good measure'); - fclose($this->socket); - $this->connect_auto(false); - if (count($this->topics)) { - $this->subscribe($this->topics); - } - } - - $byte = $this->read(1, true); - - if ((string)$byte === '') { - if ($loop === true) { - usleep(100000); - } - } else { - $cmd = (int)(ord($byte) / 16); - $this->_debugMessage( - sprintf( - 'Received CMD: %d (%s)', - $cmd, - isset(static::$known_commands[$cmd]) === true ? static::$known_commands[$cmd] : 'Unknown' - ) - ); - - $multiplier = 1; - $value = 0; - do { - $digit = ord($this->read(1)); - $value += ($digit & 127) * $multiplier; - $multiplier *= 128; - } while (($digit & 128) !== 0); - - $this->_debugMessage('Fetching: ' . $value . ' bytes'); - - $string = $value > 0 ? $this->read($value) : ''; - - if ($cmd) { - switch ($cmd) { - case 3: //Publish MSG - $return = $this->message($string); - if (is_bool($return) === false) { - return $return; - } - break; - } - } - } - - if ($this->timesinceping < (time() - $this->keepalive)) { - $this->_debugMessage('not had something in a while so ping'); - $this->ping(); - } - - if ($this->timesinceping < (time() - ($this->keepalive * 2))) { - $this->_debugMessage('not seen a packet in a while, disconnecting/reconnecting'); - fclose($this->socket); - $this->connect_auto(false); - if (count($this->topics)) { - $this->subscribe($this->topics); - } - } - - return true; - } - - /** - * Gets the length of a msg, (and increments $i) - * - * @param $msg - * @param $i - * - * @return float|int - */ - protected function getmsglength(&$msg, &$i) - { - $multiplier = 1; - $value = 0; - do { - $digit = ord($msg[$i]); - $value += ($digit & 127) * $multiplier; - $multiplier *= 128; - $i++; - } while (($digit & 128) !== 0); - - return $value; - } - - /** - * @param $len - * - * @return string - */ - protected function setmsglength($len): string - { - $string = ''; - do { - $digit = $len % 128; - $len >>= 7; - // if there are more digits to encode, set the top bit of this digit - if ($len > 0) { - $digit |= 0x80; - } - $string .= chr($digit); - } while ($len > 0); - return $string; - } - - /** - * @param $str - * @param $i - * - * @return string - */ - protected function strwritestring($str, &$i): string - { - $len = strlen($str); - $msb = $len >> 8; - $lsb = $len % 256; - $ret = chr($msb); - $ret .= chr($lsb); - $ret .= $str; - $i += ($len + 2); - return $ret; - } - - /** - * Prints a sting out character by character - * - * @param $string - */ - public function printstr($string): void - { - $strlen = strlen($string); - for ($j = 0; $j < $strlen; $j++) { - $num = ord($string[$j]); - if ($num > 31) { - $chr = $string[$j]; - } else { - $chr = ' '; - } - printf("%4d: %08b : 0x%02x : %s \n", $j, $num, $num, $chr); - } - } - - /** - * @param string $message - */ - protected function _debugMessage(string $message): void - { - if ($this->debug === true) { - echo date('r: ') . $message . PHP_EOL; - } - } - - /** - * @param string $message - */ - protected function _errorMessage(string $message): void - { - error_log('Error:' . $message); - } -} + 'CONNECT', + 2 => 'CONNACK', + 3 => 'PUBLISH', + 4 => 'PUBACK', + 5 => 'PUBREC', + 6 => 'PUBREL', + 7 => 'PUBCOMP', + 8 => 'SUBSCRIBE', + 9 => 'SUBACK', + 10 => 'UNSUBSCRIBE', + 11 => 'UNSUBACK', + 12 => 'PINGREQ', + 13 => 'PINGRESP', + 14 => 'DISCONNECT' + ]; + + /** + * phpMQTT constructor. + * + * @param $address + * @param $port + * @param $clientid + * @param null $cafile + */ + public function __construct($address, $port, $clientid, $cafile = null) + { + $this->broker($address, $port, $clientid, $cafile); + } + + /** + * Sets the broker details + * + * @param $address + * @param $port + * @param $clientid + * @param null $cafile + */ + public function broker($address, $port, $clientid, $cafile = null): void + { + $this->address = $address; + $this->port = $port; + $this->clientid = $clientid; + $this->cafile = $cafile; + } + + /** + * Will try and connect, if fails it will sleep 10s and try again, this will enable the script to recover from a network outage + * + * @param bool $clean - should the client send a clean session flag + * @param null $will + * @param null $username + * @param null $password + * + * @return bool + */ + public function connect_auto($clean = true, $will = null, $username = null, $password = null): bool + { + while ($this->connect($clean, $will, $username, $password) === false) { + sleep(10); + } + return true; + } + + /** + * @param bool $clean - should the client send a clean session flag + * @param null $will + * @param null $username + * @param null $password + * + * @return bool + */ + public function connect($clean = true, $will = null, $username = null, $password = null): bool + { + if ($will) { + $this->will = $will; + } + if ($username) { + $this->username = $username; + } + if ($password) { + $this->password = $password; + } + + if ($this->cafile) { + $socketContext = stream_context_create( + [ + 'ssl' => [ + 'verify_peer_name' => true, + 'cafile' => $this->cafile + ] + ] + ); + $this->socket = stream_socket_client('tls://' . $this->address . ':' . $this->port, $errno, $errstr, 60, STREAM_CLIENT_CONNECT, $socketContext); + } else { + $this->socket = stream_socket_client('tcp://' . $this->address . ':' . $this->port, $errno, $errstr, 60, STREAM_CLIENT_CONNECT); + } + + if (!$this->socket) { + $this->_errorMessage("stream_socket_create() $errno, $errstr"); + return false; + } + + stream_set_timeout($this->socket, 5); + stream_set_blocking($this->socket, 0); + + $i = 0; + $buffer = ''; + + $buffer .= chr(0x00); + $i++; // Length MSB + $buffer .= chr(0x04); + $i++; // Length LSB + $buffer .= chr(0x4d); + $i++; // M + $buffer .= chr(0x51); + $i++; // Q + $buffer .= chr(0x54); + $i++; // T + $buffer .= chr(0x54); + $i++; // T + $buffer .= chr(0x04); + $i++; // // Protocol Level + + //No Will + $var = 0; + if ($clean) { + $var += 2; + } + + //Add will info to header + if ($this->will !== null) { + $var += 4; // Set will flag + $var += ($this->will['qos'] << 3); //Set will qos + if ($this->will['retain']) { + $var += 32; + } //Set will retain + } + + if ($this->username !== null) { + $var += 128; + } //Add username to header + if ($this->password !== null) { + $var += 64; + } //Add password to header + + $buffer .= chr($var); + $i++; + + //Keep alive + $buffer .= chr($this->keepalive >> 8); + $i++; + $buffer .= chr($this->keepalive & 0xff); + $i++; + + $buffer .= $this->strwritestring($this->clientid, $i); + + //Adding will to payload + if ($this->will !== null) { + $buffer .= $this->strwritestring($this->will['topic'], $i); + $buffer .= $this->strwritestring($this->will['content'], $i); + } + + if ($this->username !== null) { + $buffer .= $this->strwritestring($this->username, $i); + } + if ($this->password !== null) { + $buffer .= $this->strwritestring($this->password, $i); + } + + $head = chr(0x10); + + while ($i > 0) { + $encodedByte = $i % 128; + $i /= 128; + $i = (int)$i; + if ($i > 0) { + $encodedByte |= 128; + } + $head .= chr($encodedByte); + } + + fwrite($this->socket, $head, 2); + fwrite($this->socket, $buffer); + + $string = $this->read(4); + + if (ord($string[0]) >> 4 === 2 && $string[3] === chr(0)) { + $this->_debugMessage('Connected to Broker'); + } else { + $this->_errorMessage( + sprintf( + "Connection failed! (Error: 0x%02x 0x%02x)\n", + ord($string[0]), + ord($string[3]) + ) + ); + return false; + } + + $this->timesinceping = time(); + + return true; + } + + /** + * Reads in so many bytes + * + * @param int $int + * @param bool $nb + * + * @return false|string + */ + public function read($int = 8192, $nb = false) + { + $string = ''; + $togo = $int; + + if ($nb) { + return fread($this->socket, $togo); + } + + while (!feof($this->socket) && $togo > 0) { + $fread = fread($this->socket, $togo); + $string .= $fread; + $togo = $int - strlen($string); + } + + return $string; + } + + /** + * Subscribes to a topic, wait for message and return it + * + * @param $topic + * @param $qos + * + * @return string + */ + public function subscribeAndWaitForMessage($topic, $qos): string + { + $this->subscribe( + [ + $topic => [ + 'qos' => $qos, + 'function' => '__direct_return_message__' + ] + ] + ); + + do { + $return = $this->proc(); + } while ($return === true); + + return $return; + } + + /** + * subscribes to topics + * + * @param $topics + * @param int $qos + */ + public function subscribe($topics, $qos = 0): void + { + $i = 0; + $buffer = ''; + $id = $this->msgid; + $buffer .= chr($id >> 8); + $i++; + $buffer .= chr($id % 256); + $i++; + + foreach ($topics as $key => $topic) { + $buffer .= $this->strwritestring($key, $i); + $buffer .= chr($topic['qos']); + $i++; + $this->topics[$key] = $topic; + } + + $cmd = 0x82; + //$qos + $cmd += ($qos << 1); + + $head = chr($cmd); + $head .= $this->setmsglength($i); + fwrite($this->socket, $head, strlen($head)); + + $this->_fwrite($buffer); + $string = $this->read(2); + + $bytes = ord(substr($string, 1, 1)); + $this->read($bytes); + } + + /** + * Sends a keep alive ping + */ + public function ping(): void + { + $head = chr(0xc0); + $head .= chr(0x00); + fwrite($this->socket, $head, 2); + $this->timesinceping = time(); + $this->_debugMessage('ping sent'); + } + + /** + * sends a proper disconnect cmd + */ + public function disconnect(): void + { + $head = ' '; + $head[0] = chr(0xe0); + $head[1] = chr(0x00); + fwrite($this->socket, $head, 2); + } + + /** + * Sends a proper disconnect, then closes the socket + */ + public function close(): void + { + $this->disconnect(); + stream_socket_shutdown($this->socket, STREAM_SHUT_WR); + } + + /** + * Publishes $content on a $topic + * + * @param $topic + * @param $content + * @param int $qos + * @param bool $retain + */ + public function publish($topic, $content, $qos = 0, $retain = false): void + { + $i = 0; + $buffer = ''; + + $buffer .= $this->strwritestring($topic, $i); + + if ($qos) { + $id = $this->msgid++; + $buffer .= chr($id >> 8); + $i++; + $buffer .= chr($id % 256); + $i++; + } + + $buffer .= $content; + $i += strlen($content); + + $head = ' '; + $cmd = 0x30; + if ($qos) { + $cmd += $qos << 1; + } + if (empty($retain) === false) { + ++$cmd; + } + + $head[0] = chr($cmd); + $head .= $this->setmsglength($i); + + fwrite($this->socket, $head, strlen($head)); + $this->_fwrite($buffer); + } + + /** + * Writes a string to the socket + * + * @param $buffer + * + * @return bool|int + */ + protected function _fwrite($buffer) + { + $buffer_length = strlen($buffer); + for ($written = 0; $written < $buffer_length; $written += $fwrite) { + $fwrite = fwrite($this->socket, substr($buffer, $written)); + if ($fwrite === false) { + return false; + } + } + return $buffer_length; + } + + /** + * Processes a received topic + * + * @param $msg + * + * @retrun bool|string + */ + public function message($msg) + { + $tlen = (ord($msg[0]) << 8) + ord($msg[1]); + $topic = substr($msg, 2, $tlen); + $msg = substr($msg, ($tlen + 2)); + $found = false; + foreach ($this->topics as $key => $top) { + if (preg_match( + '/^' . str_replace( + '#', + '.*', + str_replace( + '+', + "[^\/]*", + str_replace( + '/', + "\/", + str_replace( + '$', + '\$', + $key + ) + ) + ) + ) . '$/', + $topic + )) { + $found = true; + + if ($top['function'] === '__direct_return_message__') { + return $msg; + } + + if (is_callable($top['function'])) { + call_user_func($top['function'], $topic, $msg); + } else { + $this->_errorMessage('Message received on topic ' . $topic . ' but function is not callable.'); + } + } + } + + if ($found === false) { + $this->_debugMessage('msg received but no match in subscriptions'); + } + + return $found; + } + + /** + * The processing loop for an "always on" client + * set true when you are doing other stuff in the loop good for + * watching something else at the same time + * + * @param bool $loop + * + * @return bool | string + */ + public function proc(bool $loop = true) + { + if (feof($this->socket)) { + $this->_debugMessage('eof receive going to reconnect for good measure'); + fclose($this->socket); + $this->connect_auto(false); + if (count($this->topics)) { + $this->subscribe($this->topics); + } + } + + $byte = $this->read(1, true); + + if ((string)$byte === '') { + if ($loop === true) { + usleep(100000); + } + } else { + $cmd = (int)(ord($byte) / 16); + $this->_debugMessage( + sprintf( + 'Received CMD: %d (%s)', + $cmd, + isset(static::$known_commands[$cmd]) === true ? static::$known_commands[$cmd] : 'Unknown' + ) + ); + + $multiplier = 1; + $value = 0; + do { + $digit = ord($this->read(1)); + $value += ($digit & 127) * $multiplier; + $multiplier *= 128; + } while (($digit & 128) !== 0); + + $this->_debugMessage('Fetching: ' . $value . ' bytes'); + + $string = $value > 0 ? $this->read($value) : ''; + + if ($cmd) { + switch ($cmd) { + case 3: //Publish MSG + $return = $this->message($string); + if (is_bool($return) === false) { + return $return; + } + break; + } + } + } + + if ($this->timesinceping < (time() - $this->keepalive)) { + $this->_debugMessage('not had something in a while so ping'); + $this->ping(); + } + + if ($this->timesinceping < (time() - ($this->keepalive * 2))) { + $this->_debugMessage('not seen a packet in a while, disconnecting/reconnecting'); + fclose($this->socket); + $this->connect_auto(false); + if (count($this->topics)) { + $this->subscribe($this->topics); + } + } + + return true; + } + + /** + * Gets the length of a msg, (and increments $i) + * + * @param $msg + * @param $i + * + * @return float|int + */ + protected function getmsglength(&$msg, &$i) + { + $multiplier = 1; + $value = 0; + do { + $digit = ord($msg[$i]); + $value += ($digit & 127) * $multiplier; + $multiplier *= 128; + $i++; + } while (($digit & 128) !== 0); + + return $value; + } + + /** + * @param $len + * + * @return string + */ + protected function setmsglength($len): string + { + $string = ''; + do { + $digit = $len % 128; + $len >>= 7; + // if there are more digits to encode, set the top bit of this digit + if ($len > 0) { + $digit |= 0x80; + } + $string .= chr($digit); + } while ($len > 0); + return $string; + } + + /** + * @param $str + * @param $i + * + * @return string + */ + protected function strwritestring($str, &$i): string + { + $len = strlen($str); + $msb = $len >> 8; + $lsb = $len % 256; + $ret = chr($msb); + $ret .= chr($lsb); + $ret .= $str; + $i += ($len + 2); + return $ret; + } + + /** + * Prints a sting out character by character + * + * @param $string + */ + public function printstr($string): void + { + $strlen = strlen($string); + for ($j = 0; $j < $strlen; $j++) { + $num = ord($string[$j]); + if ($num > 31) { + $chr = $string[$j]; + } else { + $chr = ' '; + } + printf("%4d: %08b : 0x%02x : %s \n", $j, $num, $num, $chr); + } + } + + /** + * @param string $message + */ + protected function _debugMessage(string $message): void + { + if ($this->debug === true) { + echo date('r: ') . $message . PHP_EOL; + } + } + + /** + * @param string $message + */ + protected function _errorMessage(string $message): void + { + error_log('Error:' . $message); + } +} diff --git a/ajax/tahoma.php b/ajax/tahoma.php index c4e0175..e618345 100644 --- a/ajax/tahoma.php +++ b/ajax/tahoma.php @@ -1,252 +1,252 @@ - $dev){ - $start = strpos(strtolower($dev["name"]),strtolower($filter)); - if($start !== false){ - $dev["name"] = str_replace("u_","",$dev["name"]); - $ret[$i]["name"] = $prefix.substr($dev["name"],strlen($filter)); - $ret[$i]["id"] = $key; - $i++; - } - } - return $ret; -} - -function searchForDevice($device){ - $dev = false; - $devices = json_decode(file_get_contents($GLOBALS["tahoma_devlist"]), true); - if (strlen($device) < 3 && intval($device) >= 0 && intval($device) < sizeof($devices)) { - $dev = $devices[intval($device)]; - } else if ($key = array_search($device, array_column($devices, "name"))) { - $dev = $devices[$key]; - } else if ($key = array_search($device, array_column($devices, "id"))) { - $dev = $devices[$key]; - } else { - return false; - } - return $dev; -} - -if ($_GET["action"] == "devlist") { - $jalousien = array(); - $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices'; - $ret = getSSLPage($url,$tahoma_token); - $devices = json_decode($ret, true); - $i = 0; - foreach ($devices as $device) { - if ($device["controllableName"] == "io:ExteriorVenetianBlindIOComponent") { - $jalousien[$i]["name"] = $device["label"]; - $jalousien[$i]["id"] = $device["deviceURL"]; - $i = $i + 1; - echo $device["label"] . "(".$device["deviceURL"].")
"; - } - } - //$file = fopen($tahoma_devlist, 'w'); - //fwrite($file, json_encode($jalousien)); - //fclose($file); - echo json_encode($jalousien); -}elseif ($_GET["action"] == "pos" && isset($_GET["device"])) { - $dev = searchForDevice($_GET["device"])["id"]; - $devices = json_decode(file_get_contents($tahoma_devlist), true); - $jalousien = array(); - $url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states"; - $ret = getSSLPage($url,$tahoma_token); - $states = json_decode($ret, true); - $out = "{"; - foreach ($states as $state){ - if ($state["name"] == "core:SlateOrientationState") - $out .= "\"rotation\":".$state["value"].","; - if ($state["name"] == "core:ClosureState") - $out .= "\"position\":".$state["value"].","; - } - $out = substr($out,0,-1); - $out .= "}"; - echo $out; - header('Connection: close'); - header('Content-Length: '.ob_get_length()); - ob_end_flush(); - @ob_flush(); - flush(); - fastcgi_finish_request(); -}elseif ($_GET["action"] == "moving" && isset($_GET["device"])) { - $dev = searchForDevice($_GET["device"])["id"]; - $devices = json_decode(file_get_contents($tahoma_devlist), true); - $jalousien = array(); - $url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states/core:MovingState"; - $ret = getSSLPage($url,$tahoma_token); - $moving = json_decode($ret, true); - if($moving["value"]) - echo "true"; - else - echo "false"; - header('Connection: close'); - header('Content-Length: '.ob_get_length()); - ob_end_flush(); - @ob_flush(); - flush(); - fastcgi_finish_request(); -}elseif ($_GET["action"] == "move" && isset($_GET["pos"]) && isset($_GET["angle"]) && isset($_GET["device"])) { - $angle = $_GET["angle"]; - $pos = $_GET["pos"]; - if ($pos < 0 || $pos > 100) { - echo "Position out of Range"; - exit; - } - if ($angle < 0 || $angle > 100) { - echo "Angle out of Range"; - exit; - } - $dev = searchForDevice($_GET["device"])["id"]; - if($dev == false){ - exit; - } - header('Connection: close'); - header('Content-Length: '.ob_get_length()); - $url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states/core:MovingState"; - $ret = json_decode(getSSLPage($url,$tahoma_token),true); - - $action = array(); - $action["label"] = "myAction"; - $action["actions"] = array(); - $action["actions"][0]["deviceURL"] = $dev; - $action["actions"][0]["commands"] = array(); - if(!isset($ret["value"])){ - $ret = json_decode(getSSLPage($url,$tahoma_token),true); - } - - if(!isset($ret["value"]) || $ret["value"] == false){ - $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/exec/apply'; - echo "start"; - ob_end_flush(); - @ob_flush(); - flush(); - fastcgi_finish_request(); - $action["actions"][0]["commands"][0]["name"] = "setClosureAndOrientation"; - $action["actions"][0]["commands"][0]["parameters"][0] = intval($pos); - $action["actions"][0]["commands"][0]["parameters"][1] = intval($angle); - $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); - if(!isset($res["execId"])){ - sleep(1); - $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); //2nd try - } - }elseif($ret["value"] == true){ - $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/exec/apply'; - echo "stop"; - ob_end_flush(); - @ob_flush(); - flush(); - fastcgi_finish_request(); - $action["actions"][0]["commands"][0]["name"] = "stop"; - $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); - if(!isset($res["execId"])){ - sleep(1); - $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); //2nd try - } - } -} - - - - -if ($_GET["action"] == "myactors") { - if(isset($_GET["filter"])){ - $host = $_GET["filter"]; - $host = str_replace("-", "", strtolower($host)); - }else{ - $host = gethostbyaddr("192.168.179.32"); - $host = str_replace("-", "", strtolower(substr($host, 0, strpos($host, ".")))); - } - $devices = json_decode(file_get_contents($tahoma_devlist), true); - switch ($host) { - case "tmpegbad": - echo json_encode(filter_devs($devices, "bad ")); - break; - case "tmpegwozi": - echo json_encode(filter_devs($devices, "wozi ")); - break; - case "tmpegflorian": //flori - $ret = filter_devs($devices, "florian ","Flori "); - $ret = array_merge($ret, filter_devs($devices, "magdalena ","Magdalena ")); - echo json_encode($ret); - break; - case "tmpegmagdalena": - echo json_encode(filter_devs($devices, "magdalena ")); - break; - case "tmpegschlafzimmer": - echo json_encode(filter_devs($devices, "schlafzimmer ")); - break; - default: - $ret2=Array(); - //$ret2[0]["name"] = $host; - //$ret2[0]["id"] = 99; - $ret = filter_devs($devices, ""); - //$ret = array_merge($ret, $ret2); - echo json_encode($ret); - break; - } - header('Connection: close'); - header('Content-Length: '.ob_get_length()); - ob_end_flush(); - @ob_flush(); - flush(); - fastcgi_finish_request(); - - - //echo postSSLPage($url,json_encode($action)); -} + $dev){ + $start = strpos(strtolower($dev["name"]),strtolower($filter)); + if($start !== false){ + $dev["name"] = str_replace("u_","",$dev["name"]); + $ret[$i]["name"] = $prefix.substr($dev["name"],strlen($filter)); + $ret[$i]["id"] = $key; + $i++; + } + } + return $ret; +} + +function searchForDevice($device){ + $dev = false; + $devices = json_decode(file_get_contents($GLOBALS["tahoma_devlist"]), true); + if (strlen($device) < 3 && intval($device) >= 0 && intval($device) < sizeof($devices)) { + $dev = $devices[intval($device)]; + } else if ($key = array_search($device, array_column($devices, "name"))) { + $dev = $devices[$key]; + } else if ($key = array_search($device, array_column($devices, "id"))) { + $dev = $devices[$key]; + } else { + return false; + } + return $dev; +} + +if ($_GET["action"] == "devlist") { + $jalousien = array(); + $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices'; + $ret = getSSLPage($url,$tahoma_token); + $devices = json_decode($ret, true); + $i = 0; + foreach ($devices as $device) { + if ($device["controllableName"] == "io:ExteriorVenetianBlindIOComponent") { + $jalousien[$i]["name"] = $device["label"]; + $jalousien[$i]["id"] = $device["deviceURL"]; + $i = $i + 1; + echo $device["label"] . "(".$device["deviceURL"].")
"; + } + } + //$file = fopen($tahoma_devlist, 'w'); + //fwrite($file, json_encode($jalousien)); + //fclose($file); + echo json_encode($jalousien); +}elseif ($_GET["action"] == "pos" && isset($_GET["device"])) { + $dev = searchForDevice($_GET["device"])["id"]; + $devices = json_decode(file_get_contents($tahoma_devlist), true); + $jalousien = array(); + $url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states"; + $ret = getSSLPage($url,$tahoma_token); + $states = json_decode($ret, true); + $out = "{"; + foreach ($states as $state){ + if ($state["name"] == "core:SlateOrientationState") + $out .= "\"rotation\":".$state["value"].","; + if ($state["name"] == "core:ClosureState") + $out .= "\"position\":".$state["value"].","; + } + $out = substr($out,0,-1); + $out .= "}"; + echo $out; + header('Connection: close'); + header('Content-Length: '.ob_get_length()); + ob_end_flush(); + @ob_flush(); + flush(); + fastcgi_finish_request(); +}elseif ($_GET["action"] == "moving" && isset($_GET["device"])) { + $dev = searchForDevice($_GET["device"])["id"]; + $devices = json_decode(file_get_contents($tahoma_devlist), true); + $jalousien = array(); + $url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states/core:MovingState"; + $ret = getSSLPage($url,$tahoma_token); + $moving = json_decode($ret, true); + if($moving["value"]) + echo "true"; + else + echo "false"; + header('Connection: close'); + header('Content-Length: '.ob_get_length()); + ob_end_flush(); + @ob_flush(); + flush(); + fastcgi_finish_request(); +}elseif ($_GET["action"] == "move" && isset($_GET["pos"]) && isset($_GET["angle"]) && isset($_GET["device"])) { + $angle = $_GET["angle"]; + $pos = $_GET["pos"]; + if ($pos < 0 || $pos > 100) { + echo "Position out of Range"; + exit; + } + if ($angle < 0 || $angle > 100) { + echo "Angle out of Range"; + exit; + } + $dev = searchForDevice($_GET["device"])["id"]; + if($dev == false){ + exit; + } + header('Connection: close'); + header('Content-Length: '.ob_get_length()); + $url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states/core:MovingState"; + $ret = json_decode(getSSLPage($url,$tahoma_token),true); + + $action = array(); + $action["label"] = "myAction"; + $action["actions"] = array(); + $action["actions"][0]["deviceURL"] = $dev; + $action["actions"][0]["commands"] = array(); + if(!isset($ret["value"])){ + $ret = json_decode(getSSLPage($url,$tahoma_token),true); + } + + if(!isset($ret["value"]) || $ret["value"] == false){ + $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/exec/apply'; + echo "start"; + ob_end_flush(); + @ob_flush(); + flush(); + fastcgi_finish_request(); + $action["actions"][0]["commands"][0]["name"] = "setClosureAndOrientation"; + $action["actions"][0]["commands"][0]["parameters"][0] = intval($pos); + $action["actions"][0]["commands"][0]["parameters"][1] = intval($angle); + $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); + if(!isset($res["execId"])){ + sleep(1); + $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); //2nd try + } + }elseif($ret["value"] == true){ + $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/exec/apply'; + echo "stop"; + ob_end_flush(); + @ob_flush(); + flush(); + fastcgi_finish_request(); + $action["actions"][0]["commands"][0]["name"] = "stop"; + $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); + if(!isset($res["execId"])){ + sleep(1); + $res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); //2nd try + } + } +} + + + + +if ($_GET["action"] == "myactors") { + if(isset($_GET["filter"])){ + $host = $_GET["filter"]; + $host = str_replace("-", "", strtolower($host)); + }else{ + $host = gethostbyaddr("192.168.179.32"); + $host = str_replace("-", "", strtolower(substr($host, 0, strpos($host, ".")))); + } + $devices = json_decode(file_get_contents($tahoma_devlist), true); + switch ($host) { + case "tmpegbad": + echo json_encode(filter_devs($devices, "bad ")); + break; + case "tmpegwozi": + echo json_encode(filter_devs($devices, "wozi ")); + break; + case "tmpegflorian": //flori + $ret = filter_devs($devices, "florian ","Flori "); + $ret = array_merge($ret, filter_devs($devices, "magdalena ","Magdalena ")); + echo json_encode($ret); + break; + case "tmpegmagdalena": + echo json_encode(filter_devs($devices, "magdalena ")); + break; + case "tmpegschlafzimmer": + echo json_encode(filter_devs($devices, "schlafzimmer ")); + break; + default: + $ret2=Array(); + //$ret2[0]["name"] = $host; + //$ret2[0]["id"] = 99; + $ret = filter_devs($devices, ""); + //$ret = array_merge($ret, $ret2); + echo json_encode($ret); + break; + } + header('Connection: close'); + header('Content-Length: '.ob_get_length()); + ob_end_flush(); + @ob_flush(); + flush(); + fastcgi_finish_request(); + + + //echo postSSLPage($url,json_encode($action)); +} diff --git a/assets/fonts/font_poppins.css b/assets/fonts/font_poppins.css index a6656bd..c10f113 100644 --- a/assets/fonts/font_poppins.css +++ b/assets/fonts/font_poppins.css @@ -1,144 +1,144 @@ -/* devanagari */ -@font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 200; - src: url(pxiByp8kv8JHgFVrLFj_Z11lFc-K.woff2) format('woff2'); - unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; - } - /* latin-ext */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 200; - src: url(pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; - } - /* latin */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 200; - src: url(pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - /* devanagari */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 300; - src: url(pxiByp8kv8JHgFVrLDz8Z11lFc-K.woff2) format('woff2'); - unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; - } - /* latin-ext */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 300; - src: url(pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; - } - /* latin */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 300; - src: url(pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - /* devanagari */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 400; - src: url(pxiEyp8kv8JHgFVrJJbecmNE.woff2) format('woff2'); - unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; - } - /* latin-ext */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 400; - src: url(pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; - } - /* latin */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 400; - src: url(pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - /* devanagari */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 600; - src: url(pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2) format('woff2'); - unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; - } - /* latin-ext */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 600; - src: url(pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; - } - /* latin */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 600; - src: url(pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - /* devanagari */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 700; - src: url(pxiByp8kv8JHgFVrLCz7Z11lFc-K.woff2) format('woff2'); - unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; - } - /* latin-ext */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 700; - src: url(pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; - } - /* latin */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 700; - src: url(pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - /* devanagari */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 800; - src: url(pxiByp8kv8JHgFVrLDD4Z11lFc-K.woff2) format('woff2'); - unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; - } - /* latin-ext */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 800; - src: url(pxiByp8kv8JHgFVrLDD4Z1JlFc-K.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; - } - /* latin */ - @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 800; - src: url(pxiByp8kv8JHgFVrLDD4Z1xlFQ.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +/* devanagari */ +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 200; + src: url(pxiByp8kv8JHgFVrLFj_Z11lFc-K.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; + } + /* latin-ext */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 200; + src: url(pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + } + /* latin */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 200; + src: url(pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + /* devanagari */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 300; + src: url(pxiByp8kv8JHgFVrLDz8Z11lFc-K.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; + } + /* latin-ext */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 300; + src: url(pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + } + /* latin */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 300; + src: url(pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + /* devanagari */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url(pxiEyp8kv8JHgFVrJJbecmNE.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; + } + /* latin-ext */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url(pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + } + /* latin */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url(pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + /* devanagari */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + src: url(pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; + } + /* latin-ext */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + src: url(pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + } + /* latin */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + src: url(pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + /* devanagari */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 700; + src: url(pxiByp8kv8JHgFVrLCz7Z11lFc-K.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; + } + /* latin-ext */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 700; + src: url(pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + } + /* latin */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 700; + src: url(pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + /* devanagari */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 800; + src: url(pxiByp8kv8JHgFVrLDD4Z11lFc-K.woff2) format('woff2'); + unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09; + } + /* latin-ext */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 800; + src: url(pxiByp8kv8JHgFVrLDD4Z1JlFc-K.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + } + /* latin */ + @font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 800; + src: url(pxiByp8kv8JHgFVrLDD4Z1xlFQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } \ No newline at end of file diff --git a/assets/img/arrow.svg b/assets/img/arrow.svg index b41d749..7b55297 100644 --- a/assets/img/arrow.svg +++ b/assets/img/arrow.svg @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/assets/img/heater.svg b/assets/img/heater.svg index 10937cf..a67e0fe 100644 --- a/assets/img/heater.svg +++ b/assets/img/heater.svg @@ -1,287 +1,287 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --.- °C - - - --.- °C - - - --.- °C - - - --.- °C - - - --.- °C - - - --.- °C - - - --.- °C - - - --.- °C - - - --.- °C - - - --.- °C - - - Triac: - - - --.- °C - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --.- °C + + + --.- °C + + + --.- °C + + + --.- °C + + + --.- °C + + + --.- °C + + + --.- °C + + + --.- °C + + + --.- °C + + + --.- °C + + + Triac: + + + --.- °C + + \ No newline at end of file diff --git a/assets/img/realtime.svg b/assets/img/realtime.svg index 6bdb8f5..2aecb3b 100644 --- a/assets/img/realtime.svg +++ b/assets/img/realtime.svg @@ -1,893 +1,893 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --.-- kW - --.-- kW - --.-- kW - --W - - - - - - - - - - - - - - - - - - - - - - --.-- kW - --°C - --°C - - - - - - - - - - - - - - - - - - - - - - - --.-- kW - - - - - - - - - - - - - - - - - - - - - - - - - - - - --.-- kW - --% - --% - - - - - - - - - - - - - - - - - - - --.-- kW - - - - - - - - - - - - - - --.-- kW - - - - - - - - - - - - - Vorn: aus - - - Tröge: aus - - - Beete: aus - - - Aus - Aus - Aus - - - - - - - - - - - - --.-- kW - - - - - - - - - - - - - - --.-- kW - --.-- kW - - - - - - - - - - - - - - - - --.-- kW - --.- % - - - - - - - - - - - --.-- kW - --.- A - --.- A - --.- A - - - - - - - - - - Autarkie: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -.-- kW - - - - - - - - - - - - -.-- kW - - - #19 - - #19 - - #18 - - #17 - - #16 - - #15 - - #14 - - #13 - - #12 - - #11 - - #10 - - #9 - - #8 - - #7 - - #6 - - #5 - - #1 - - #2 - - #3 - - #4 - - - - W - - - W - - - - W - - - W - - - - W - - - W - - - - W - - - W - - - - W - - - W - - - - W - - - - W - - - - - - - - - - Tooltip - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --.-- kW + --.-- kW + --.-- kW + --W + + + + + + + + + + + + + + + + + + + + + + --.-- kW + --°C + --°C + + + + + + + + + + + + + + + + + + + + + + + --.-- kW + + + + + + + + + + + + + + + + + + + + + + + + + + + + --.-- kW + --% + --% + + + + + + + + + + + + + + + + + + + --.-- kW + + + + + + + + + + + + + + --.-- kW + + + + + + + + + + + + + Vorn: aus + + + Tröge: aus + + + Beete: aus + + + Aus + Aus + Aus + + + + + + + + + + + + --.-- kW + + + + + + + + + + + + + + --.-- kW + --.-- kW + + + + + + + + + + + + + + + + --.-- kW + --.- % + + + + + + + + + + + --.-- kW + --.- A + --.- A + --.- A + + + + + + + + + + Autarkie: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -.-- kW + + + + + + + + + + + + -.-- kW + + + #19 + + #19 + + #18 + + #17 + + #16 + + #15 + + #14 + + #13 + + #12 + + #11 + + #10 + + #9 + + #8 + + #7 + + #6 + + #5 + + #1 + + #2 + + #3 + + #4 + + + - W + + - W + + + - W + + - W + + + - W + + - W + + + - W + + - W + + + - W + + - W + + + - W + + + - W + + + + + + + + + + Tooltip + \ No newline at end of file diff --git a/authServer.php b/authServer.php index d0ade36..c78c825 100644 --- a/authServer.php +++ b/authServer.php @@ -1,366 +1,366 @@ - getCreateArgs - * | - * navigator.credentials.create <-------------' - * | - * '-------------------------> processCreate - * | - * alert ok or fail <----------------' - * - * ------------------------------------------------------------ - * - * VALIDATION - * - * window.fetch ------------------> getGetArgs - * | - * navigator.credentials.get <----------------' - * | - * '-------------------------> processGet - * | - * alert ok or fail <----------------' - * - * ------------------------------------------------------------ - */ - -require_once './restricted/WebAuthn/src/WebAuthn.php'; -require_once("./helper.php"); // startet die Session, liefert isLocal() und mysql.php -try { - - // read get argument and post body - $fn = filter_input(INPUT_GET, 'fn'); - - // Registrierung und Loeschen von Anmeldedaten nur nach einem gueltigen - // Einmalschluessel aus addUser.php (checkAdduser setzt mayRegister) oder - // aus dem lokalen Netz. Ohne diese Wache genuegen zwei Aufrufe von - // beliebiger Stelle, um einen eigenen Passkey fuer das fest hinterlegte - // Konto anzulegen - der Einmalschluessel schuetzte nur die Anzeige in - // index.php, nicht diesen Endpunkt. - if ($fn === 'getCreateArgs' || $fn === 'processCreate' || $fn === 'clearRegistrations') { - $freigegeben = isset($_SESSION['mayRegister']) - && (time() - $_SESSION['mayRegister']) < 600; - if (!$freigegeben && !isLocal()) { - http_response_code(403); - header('Content-Type: application/json'); - print(json_encode(['success' => false, 'msg' => 'Registrierung nicht freigegeben.'])); - exit; - } - } - $requireResidentKey = false; - $userVerification = false; - $formats = []; - $msg=""; - - $formats[] = 'android-key'; - $formats[] = 'android-safetynet'; - $formats[] = 'apple'; - $formats[] = 'fido-u2f'; - $formats[] = 'none'; - $formats[] = 'packed'; - $formats[] = 'tpm'; - $userId = "E071229F004A4CDE"; - $userName = "SmartHomeWagner"; - $userDisplayName = "2025Bym0"; - - $post = trim(file_get_contents('php://input')); - if ($post) { - $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); - } - - $rpId = "nas.el-wa.org"; - if ($rpId === false) { - throw new Exception('invalid relying party ID'); - } - - // cross-platform: true, if type internal is not allowed - // false, if only internal is allowed - // null, if internal and cross-platform is allowed - $crossPlatformAttachment = null; - - // new Instance of the server library. - // make sure that $rpId is the domain name. - $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/isrg-root-x2.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solo.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solokey_f1.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solokey_r1.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/apple.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/yubico.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/hypersecu.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/globalSign.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/googleHardware.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/microsoftTpmCollection.pem'); - $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/mds'); - - // ------------------------------------ - // request for create arguments - // ------------------------------------ - - if ($fn === 'getCreateArgs') { - $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment); - - header('Content-Type: application/json'); - print(json_encode($createArgs)); - - // save challange to session. you have to deliver it to processGet later. - $_SESSION['challenge'] = $WebAuthn->getChallenge(); - - - - // ------------------------------------ - // request for get arguments - // ------------------------------------ - - } else if ($fn === 'getGetArgs') { - $ids = []; - $mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db); - $result = mysqli_query($mysql,"SELECT credentialId FROM users WHERE userId = '".base64_encode($userId)."';"); - if(!$result){ - $msg = "Error:
".mysqli_error($mysql)."
"; - } - if ($result->num_rows > 0) { - while($row = $result->fetch_assoc()) { - $ids[] = base64_decode($row["credentialId"]); - } - } - -/* - if ($requireResidentKey) { - if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) { - throw new Exception('we do not have any registrations in session to check the registration'); - } - - } else { - // load registrations from session stored there by processCreate. - // normaly you have to load the credential Id's for a username - // from the database. - if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { - foreach ($_SESSION['registrations'] as $reg) { - if ($reg->userId === $userId) { - $ids[] = $reg->credentialId; - } - } - } - } -*/ - if (count($ids) === 0) { - throw new Exception('no registrations in session for userId ' . $userId); - } - - $getArgs = $WebAuthn->getGetArgs($ids, 60*4, true, true, true, true, true, $userVerification); - - header('Content-Type: application/json'); - print(json_encode($getArgs)); - - // save challange to session. you have to deliver it to processGet later. - $_SESSION['challenge'] = $WebAuthn->getChallenge(); - - - - // ------------------------------------ - // process create - // ------------------------------------ - - } else if ($fn === 'processCreate') { - $mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db); - $clientDataJSON = !empty($post->clientDataJSON) ? base64_decode($post->clientDataJSON) : null; - $attestationObject = !empty($post->attestationObject) ? base64_decode($post->attestationObject) : null; - $challenge = $_SESSION['challenge'] ?? null; - - // processCreate returns data to be stored for future logins. - // in this example we store it in the php session. - // Normally you have to store the data in a database connected - // with the username. - $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false); - - // add user infos - $data->userId = $userId; - $data->userName = $userName; - $data->userDisplayName = $userDisplayName; - //set Null to 0 - $data->signatureCounter ??= 0; - /* - if (!isset($_SESSION['registrations']) || !array_key_exists('registrations', $_SESSION) || !is_array($_SESSION['registrations'])) { - $_SESSION['registrations'] = []; - }*/ - if(!mysqli_query($mysql,"INSERT INTO users SET userId = '".base64_encode($data->userId)."', credentialId = '".base64_encode($data->credentialId)."', credentialPublicKey = '".base64_encode($data->credentialPublicKey)."', signatureCounter = '".base64_encode($data->signatureCounter)."', name = '".mysqli_real_escape_string($mysql,filter_input(INPUT_GET, 'name'))."';")){ - $msg = "Error:
".mysqli_error($mysql)."
"; - } - else{ - if ($data->rootValid === false) { - $msg = 'registration ok, but certificate does not match any of the selected root ca.'; - } - // $msg = "Data: ".json_last_error();//json_encode($data);//'registration success.'; - } - /* - $_SESSION['registrations'][] = $data; -*/ - $return = new stdClass(); - $return->success = true; - $return->msg = $msg; - - header('Content-Type: application/json'); - print(json_encode($return)); - - - - // ------------------------------------ - // proccess get - // ------------------------------------ - - } else if ($fn === 'processGet') { - $clientDataJSON = !empty($post->clientDataJSON) ? base64_decode($post->clientDataJSON) : null; - $authenticatorData = !empty($post->authenticatorData) ? base64_decode($post->authenticatorData) : null; - $signature = !empty($post->signature) ? base64_decode($post->signature) : null; - $userHandle = !empty($post->userHandle) ? base64_decode($post->userHandle) : null; - $id = !empty($post->id) ? base64_decode($post->id) : null; - $challenge = $_SESSION['challenge'] ?? ''; - $credentialPublicKey = null; - - // looking up correspondending public key of the credential id - // you should also validate that only ids of the given user name - // are taken for the login. - $mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db); - $result = mysqli_query($mysql,"SELECT credentialPublicKey, userId, name FROM users WHERE credentialId = '".base64_encode($id)."';"); - if(!$result){ - $msg = "Error:
".mysqli_error($mysql)."
"; - } - if ($result->num_rows > 0) { - $row = $result->fetch_assoc(); - $credentialPublicKey = base64_decode($row["credentialPublicKey"]); - $reg = (object) ['userId' => base64_decode($row["userId"])]; - $username = $row["name"]; - } /*else { - if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { - foreach ($_SESSION['registrations'] as $reg) { - if ($reg->credentialId === $id) { - $credentialPublicKey = $reg->credentialPublicKey; - break; - } - } - } - }*/ - if ($credentialPublicKey === null) { - throw new Exception('Public Key for credential ID not found!'); - } - - // if we have resident key, we have to verify that the userHandle is the provided userId at registration - if ($requireResidentKey && $userHandle !== hex2bin($reg->userId)) { - throw new \Exception('userId doesnt match (is ' . bin2hex($userHandle) . ' but expect ' . $reg->userId . ')'); - } - - // process the get request. throws WebAuthnException if it fails - $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required'); - - $return = new stdClass(); - $return->success = true; - $authKey = strval(random_int(0,99999999)); - $result = mysqli_query($mysql,"UPDATE users SET authKey=".$authKey.", lastAuth=NOW() WHERE credentialId = '".base64_encode($id)."';"); - $_SESSION["Logged"] = true; - $_SESSION["user"] = $username; - $_SESSION["authKey"] = $authKey; - header('Content-Type: application/json'); - print(json_encode($return)); - - // ------------------------------------ - // proccess clear registrations - // ------------------------------------ - - } else if ($fn === 'clearRegistrations') { - $_SESSION['registrations'] = null; - $_SESSION['challenge'] = null; - - $return = new stdClass(); - $return->success = true; - $return->msg = 'all registrations deleted'; - - header('Content-Type: application/json'); - print(json_encode($return)); - - // ------------------------------------ - // display stored data as HTML - // ------------------------------------ - - } /*else if ($fn === 'getStoredDataHtml') { - $html = '' . "\n"; - $html .= ''; - $html .= ''; - if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { - $html .= '

There are ' . count($_SESSION['registrations']) . ' registrations in this session:

'; - foreach ($_SESSION['registrations'] as $reg) { - $html .= ''; - foreach ($reg as $key => $value) { - - if (is_bool($value)) { - $value = $value ? 'yes' : 'no'; - - } else if (is_null($value)) { - $value = 'null'; - - } else if (is_object($value)) { - $value = chunk_split(strval($value), 64); - - } else if (is_string($value) && strlen($value) > 0 && htmlspecialchars($value, ENT_QUOTES) === '') { - $value = chunk_split(bin2hex($value), 64); - } - $html .= ''; - } - $html .= '
' . htmlspecialchars($key) . '' . nl2br(htmlspecialchars($value)) . '
'; - } - } else { - $html .= '

There are no registrations.

'; - } - $html .= ''; - - header('Content-Type: text/html'); - print $html; - - // ------------------------------------ - // get root certs from FIDO Alliance Metadata Service - // ------------------------------------ - - } */else if ($fn === 'queryFidoMetaDataService') { - - $mdsFolder = './restricted/WebAuthn/rootCertificates/mds'; - $success = false; - $msg = null; - - // fetch only 1x / 24h - $lastFetch = \is_file($mdsFolder . '/lastMdsFetch.txt') ? \strtotime(\file_get_contents($mdsFolder . '/lastMdsFetch.txt')) : 0; - if ($lastFetch + (3600*48) < \time()) { - $cnt = $WebAuthn->queryFidoMetaDataService($mdsFolder); - $success = true; - \file_put_contents($mdsFolder . '/lastMdsFetch.txt', date('r')); - $msg = 'successfully queried FIDO Alliance Metadata Service - ' . $cnt . ' certificates downloaded.'; - - } else { - $msg = 'Fail: last fetch was at ' . date('r', $lastFetch) . ' - fetch only 1x every 48h'; - } - - $return = new stdClass(); - $return->success = $success; - $return->msg = $msg; - - header('Content-Type: application/json'); - print(json_encode($return)); - } - -} catch (Throwable $ex) { - $return = new stdClass(); - $return->success = false; - $return->msg = $ex->getMessage(); - - header('Content-Type: application/json'); - print(json_encode($return)); + getCreateArgs + * | + * navigator.credentials.create <-------------' + * | + * '-------------------------> processCreate + * | + * alert ok or fail <----------------' + * + * ------------------------------------------------------------ + * + * VALIDATION + * + * window.fetch ------------------> getGetArgs + * | + * navigator.credentials.get <----------------' + * | + * '-------------------------> processGet + * | + * alert ok or fail <----------------' + * + * ------------------------------------------------------------ + */ + +require_once './restricted/WebAuthn/src/WebAuthn.php'; +require_once("./helper.php"); // startet die Session, liefert isLocal() und mysql.php +try { + + // read get argument and post body + $fn = filter_input(INPUT_GET, 'fn'); + + // Registrierung und Loeschen von Anmeldedaten nur nach einem gueltigen + // Einmalschluessel aus addUser.php (checkAdduser setzt mayRegister) oder + // aus dem lokalen Netz. Ohne diese Wache genuegen zwei Aufrufe von + // beliebiger Stelle, um einen eigenen Passkey fuer das fest hinterlegte + // Konto anzulegen - der Einmalschluessel schuetzte nur die Anzeige in + // index.php, nicht diesen Endpunkt. + if ($fn === 'getCreateArgs' || $fn === 'processCreate' || $fn === 'clearRegistrations') { + $freigegeben = isset($_SESSION['mayRegister']) + && (time() - $_SESSION['mayRegister']) < 600; + if (!$freigegeben && !isLocal()) { + http_response_code(403); + header('Content-Type: application/json'); + print(json_encode(['success' => false, 'msg' => 'Registrierung nicht freigegeben.'])); + exit; + } + } + $requireResidentKey = false; + $userVerification = false; + $formats = []; + $msg=""; + + $formats[] = 'android-key'; + $formats[] = 'android-safetynet'; + $formats[] = 'apple'; + $formats[] = 'fido-u2f'; + $formats[] = 'none'; + $formats[] = 'packed'; + $formats[] = 'tpm'; + $userId = "E071229F004A4CDE"; + $userName = "SmartHomeWagner"; + $userDisplayName = "2025Bym0"; + + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); + } + + $rpId = "nas.el-wa.org"; + if ($rpId === false) { + throw new Exception('invalid relying party ID'); + } + + // cross-platform: true, if type internal is not allowed + // false, if only internal is allowed + // null, if internal and cross-platform is allowed + $crossPlatformAttachment = null; + + // new Instance of the server library. + // make sure that $rpId is the domain name. + $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/isrg-root-x2.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solo.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solokey_f1.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solokey_r1.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/apple.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/yubico.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/hypersecu.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/globalSign.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/googleHardware.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/microsoftTpmCollection.pem'); + $WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/mds'); + + // ------------------------------------ + // request for create arguments + // ------------------------------------ + + if ($fn === 'getCreateArgs') { + $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment); + + header('Content-Type: application/json'); + print(json_encode($createArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + + + // ------------------------------------ + // request for get arguments + // ------------------------------------ + + } else if ($fn === 'getGetArgs') { + $ids = []; + $mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db); + $result = mysqli_query($mysql,"SELECT credentialId FROM users WHERE userId = '".base64_encode($userId)."';"); + if(!$result){ + $msg = "Error:
".mysqli_error($mysql)."
"; + } + if ($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $ids[] = base64_decode($row["credentialId"]); + } + } + +/* + if ($requireResidentKey) { + if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) { + throw new Exception('we do not have any registrations in session to check the registration'); + } + + } else { + // load registrations from session stored there by processCreate. + // normaly you have to load the credential Id's for a username + // from the database. + if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { + foreach ($_SESSION['registrations'] as $reg) { + if ($reg->userId === $userId) { + $ids[] = $reg->credentialId; + } + } + } + } +*/ + if (count($ids) === 0) { + throw new Exception('no registrations in session for userId ' . $userId); + } + + $getArgs = $WebAuthn->getGetArgs($ids, 60*4, true, true, true, true, true, $userVerification); + + header('Content-Type: application/json'); + print(json_encode($getArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + + + // ------------------------------------ + // process create + // ------------------------------------ + + } else if ($fn === 'processCreate') { + $mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db); + $clientDataJSON = !empty($post->clientDataJSON) ? base64_decode($post->clientDataJSON) : null; + $attestationObject = !empty($post->attestationObject) ? base64_decode($post->attestationObject) : null; + $challenge = $_SESSION['challenge'] ?? null; + + // processCreate returns data to be stored for future logins. + // in this example we store it in the php session. + // Normally you have to store the data in a database connected + // with the username. + $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false); + + // add user infos + $data->userId = $userId; + $data->userName = $userName; + $data->userDisplayName = $userDisplayName; + //set Null to 0 + $data->signatureCounter ??= 0; + /* + if (!isset($_SESSION['registrations']) || !array_key_exists('registrations', $_SESSION) || !is_array($_SESSION['registrations'])) { + $_SESSION['registrations'] = []; + }*/ + if(!mysqli_query($mysql,"INSERT INTO users SET userId = '".base64_encode($data->userId)."', credentialId = '".base64_encode($data->credentialId)."', credentialPublicKey = '".base64_encode($data->credentialPublicKey)."', signatureCounter = '".base64_encode($data->signatureCounter)."', name = '".mysqli_real_escape_string($mysql,filter_input(INPUT_GET, 'name'))."';")){ + $msg = "Error:
".mysqli_error($mysql)."
"; + } + else{ + if ($data->rootValid === false) { + $msg = 'registration ok, but certificate does not match any of the selected root ca.'; + } + // $msg = "Data: ".json_last_error();//json_encode($data);//'registration success.'; + } + /* + $_SESSION['registrations'][] = $data; +*/ + $return = new stdClass(); + $return->success = true; + $return->msg = $msg; + + header('Content-Type: application/json'); + print(json_encode($return)); + + + + // ------------------------------------ + // proccess get + // ------------------------------------ + + } else if ($fn === 'processGet') { + $clientDataJSON = !empty($post->clientDataJSON) ? base64_decode($post->clientDataJSON) : null; + $authenticatorData = !empty($post->authenticatorData) ? base64_decode($post->authenticatorData) : null; + $signature = !empty($post->signature) ? base64_decode($post->signature) : null; + $userHandle = !empty($post->userHandle) ? base64_decode($post->userHandle) : null; + $id = !empty($post->id) ? base64_decode($post->id) : null; + $challenge = $_SESSION['challenge'] ?? ''; + $credentialPublicKey = null; + + // looking up correspondending public key of the credential id + // you should also validate that only ids of the given user name + // are taken for the login. + $mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db); + $result = mysqli_query($mysql,"SELECT credentialPublicKey, userId, name FROM users WHERE credentialId = '".base64_encode($id)."';"); + if(!$result){ + $msg = "Error:
".mysqli_error($mysql)."
"; + } + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + $credentialPublicKey = base64_decode($row["credentialPublicKey"]); + $reg = (object) ['userId' => base64_decode($row["userId"])]; + $username = $row["name"]; + } /*else { + if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { + foreach ($_SESSION['registrations'] as $reg) { + if ($reg->credentialId === $id) { + $credentialPublicKey = $reg->credentialPublicKey; + break; + } + } + } + }*/ + if ($credentialPublicKey === null) { + throw new Exception('Public Key for credential ID not found!'); + } + + // if we have resident key, we have to verify that the userHandle is the provided userId at registration + if ($requireResidentKey && $userHandle !== hex2bin($reg->userId)) { + throw new \Exception('userId doesnt match (is ' . bin2hex($userHandle) . ' but expect ' . $reg->userId . ')'); + } + + // process the get request. throws WebAuthnException if it fails + $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required'); + + $return = new stdClass(); + $return->success = true; + $authKey = strval(random_int(0,99999999)); + $result = mysqli_query($mysql,"UPDATE users SET authKey=".$authKey.", lastAuth=NOW() WHERE credentialId = '".base64_encode($id)."';"); + $_SESSION["Logged"] = true; + $_SESSION["user"] = $username; + $_SESSION["authKey"] = $authKey; + header('Content-Type: application/json'); + print(json_encode($return)); + + // ------------------------------------ + // proccess clear registrations + // ------------------------------------ + + } else if ($fn === 'clearRegistrations') { + $_SESSION['registrations'] = null; + $_SESSION['challenge'] = null; + + $return = new stdClass(); + $return->success = true; + $return->msg = 'all registrations deleted'; + + header('Content-Type: application/json'); + print(json_encode($return)); + + // ------------------------------------ + // display stored data as HTML + // ------------------------------------ + + } /*else if ($fn === 'getStoredDataHtml') { + $html = '' . "\n"; + $html .= ''; + $html .= ''; + if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { + $html .= '

There are ' . count($_SESSION['registrations']) . ' registrations in this session:

'; + foreach ($_SESSION['registrations'] as $reg) { + $html .= ''; + foreach ($reg as $key => $value) { + + if (is_bool($value)) { + $value = $value ? 'yes' : 'no'; + + } else if (is_null($value)) { + $value = 'null'; + + } else if (is_object($value)) { + $value = chunk_split(strval($value), 64); + + } else if (is_string($value) && strlen($value) > 0 && htmlspecialchars($value, ENT_QUOTES) === '') { + $value = chunk_split(bin2hex($value), 64); + } + $html .= ''; + } + $html .= '
' . htmlspecialchars($key) . '' . nl2br(htmlspecialchars($value)) . '
'; + } + } else { + $html .= '

There are no registrations.

'; + } + $html .= ''; + + header('Content-Type: text/html'); + print $html; + + // ------------------------------------ + // get root certs from FIDO Alliance Metadata Service + // ------------------------------------ + + } */else if ($fn === 'queryFidoMetaDataService') { + + $mdsFolder = './restricted/WebAuthn/rootCertificates/mds'; + $success = false; + $msg = null; + + // fetch only 1x / 24h + $lastFetch = \is_file($mdsFolder . '/lastMdsFetch.txt') ? \strtotime(\file_get_contents($mdsFolder . '/lastMdsFetch.txt')) : 0; + if ($lastFetch + (3600*48) < \time()) { + $cnt = $WebAuthn->queryFidoMetaDataService($mdsFolder); + $success = true; + \file_put_contents($mdsFolder . '/lastMdsFetch.txt', date('r')); + $msg = 'successfully queried FIDO Alliance Metadata Service - ' . $cnt . ' certificates downloaded.'; + + } else { + $msg = 'Fail: last fetch was at ' . date('r', $lastFetch) . ' - fetch only 1x every 48h'; + } + + $return = new stdClass(); + $return->success = $success; + $return->msg = $msg; + + header('Content-Type: application/json'); + print(json_encode($return)); + } + +} catch (Throwable $ex) { + $return = new stdClass(); + $return->success = false; + $return->msg = $ex->getMessage(); + + header('Content-Type: application/json'); + print(json_encode($return)); } \ No newline at end of file diff --git a/css/adminlte.min.css b/css/adminlte.min.css index 0fc9b8a..7a7ad06 100644 --- a/css/adminlte.min.css +++ b/css/adminlte.min.css @@ -1,7 +1,7 @@ -@charset "UTF-8";/*! - * AdminLTE v4.0.0-rc5 - * Author: Colorlib - * Website: AdminLTE.io - * License: Open source - MIT - */:root,[data-bs-theme=light]{--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-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--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-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:rgb(5.2, 44, 101.2);--bs-secondary-text-emphasis:rgb(43.2, 46.8, 50);--bs-success-text-emphasis:rgb(10, 54, 33.6);--bs-info-text-emphasis:rgb(5.2, 80.8, 96);--bs-warning-text-emphasis:rgb(102, 77.2, 2.8);--bs-danger-text-emphasis:rgb(88, 21.2, 27.6);--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:rgb(206.6, 226, 254.6);--bs-secondary-bg-subtle:rgb(225.6, 227.4, 229);--bs-success-bg-subtle:rgb(209, 231, 220.8);--bs-info-bg-subtle:rgb(206.6, 244.4, 252);--bs-warning-bg-subtle:rgb(255, 242.6, 205.4);--bs-danger-bg-subtle:rgb(248, 214.6, 217.8);--bs-light-bg-subtle:rgb(251.5, 252, 252.5);--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:rgb(158.2, 197, 254.2);--bs-secondary-border-subtle:rgb(196.2, 199.8, 203);--bs-success-border-subtle:rgb(163, 207, 186.6);--bs-info-border-subtle:rgb(158.2, 233.8, 249);--bs-warning-border-subtle:rgb(255, 230.2, 155.8);--bs-danger-border-subtle:rgb(241, 174.2, 180.6);--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:"Source Sans 3",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,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));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:rgb(10.4, 88, 202.4);--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:rgb(255, 242.6, 205.4);--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:rgb(42.5, 47.5, 52.5);--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:rgb(109.8, 168, 253.8);--bs-secondary-text-emphasis:rgb(166.8, 172.2, 177);--bs-success-text-emphasis:rgb(117, 183, 152.4);--bs-info-text-emphasis:rgb(109.8, 223.2, 246);--bs-warning-text-emphasis:rgb(255, 217.8, 106.2);--bs-danger-text-emphasis:rgb(234, 133.8, 143.4);--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:rgb(2.6, 22, 50.6);--bs-secondary-bg-subtle:rgb(21.6, 23.4, 25);--bs-success-bg-subtle:rgb(5, 27, 16.8);--bs-info-bg-subtle:rgb(2.6, 40.4, 48);--bs-warning-bg-subtle:rgb(51, 38.6, 1.4);--bs-danger-bg-subtle:rgb(44, 10.6, 13.8);--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:rgb(7.8, 66, 151.8);--bs-secondary-border-subtle:rgb(64.8, 70.2, 75);--bs-success-border-subtle:rgb(15, 81, 50.4);--bs-info-border-subtle:rgb(7.8, 121.2, 144);--bs-warning-border-subtle:rgb(153, 115.8, 4.2);--bs-danger-border-subtle:rgb(132, 31.8, 41.4);--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:rgb(109.8, 168, 253.8);--bs-link-hover-color:rgb(138.84, 185.4, 254.04);--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:rgb(230.4, 132.6, 181.2);--bs-highlight-color:#dee2e6;--bs-highlight-bg:rgb(102, 77.2, 2.8);--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:rgb(117, 183, 152.4);--bs-form-valid-border-color:rgb(117, 183, 152.4);--bs-form-invalid-color:rgb(234, 133.8, 143.4);--bs-form-invalid-border-color:rgb(234, 133.8, 143.4)}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.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;color:var(--bs-heading-color)}.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[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:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}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}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:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}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:var(--bs-secondary-color);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]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[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;line-height:inherit;font-size:calc(1.275rem + .3vw)}@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]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-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-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@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:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-sm);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);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}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1600px;--bs-breakpoint-xxl:2000px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:rgb(206.6, 226, 254.6);--bs-table-border-color:rgb(165.28, 180.8, 203.68);--bs-table-striped-bg:rgb(196.27, 214.7, 241.87);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(185.94, 203.4, 229.14);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(191.105, 209.05, 235.505);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:rgb(225.6, 227.4, 229);--bs-table-border-color:rgb(180.48, 181.92, 183.2);--bs-table-striped-bg:rgb(214.32, 216.03, 217.55);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(203.04, 204.66, 206.1);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(208.68, 210.345, 211.825);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:rgb(209, 231, 220.8);--bs-table-border-color:rgb(167.2, 184.8, 176.64);--bs-table-striped-bg:rgb(198.55, 219.45, 209.76);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(188.1, 207.9, 198.72);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(193.325, 213.675, 204.24);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:rgb(206.6, 244.4, 252);--bs-table-border-color:rgb(165.28, 195.52, 201.6);--bs-table-striped-bg:rgb(196.27, 232.18, 239.4);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(185.94, 219.96, 226.8);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(191.105, 226.07, 233.1);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:rgb(255, 242.6, 205.4);--bs-table-border-color:rgb(204, 194.08, 164.32);--bs-table-striped-bg:rgb(242.25, 230.47, 195.13);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(229.5, 218.34, 184.86);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(235.875, 224.405, 189.995);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:rgb(248, 214.6, 217.8);--bs-table-border-color:rgb(198.4, 171.68, 174.24);--bs-table-striped-bg:rgb(235.6, 203.87, 206.91);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(223.2, 193.14, 196.02);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(229.4, 198.505, 201.465);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:rgb(198.4, 199.2, 200);--bs-table-striped-bg:rgb(235.6, 236.55, 237.5);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(223.2, 224.1, 225);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(229.4, 230.325, 231.25);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:rgb(77.4, 80.6, 83.8);--bs-table-striped-bg:rgb(44.1, 47.9, 51.7);--bs-table-striped-color:#fff;--bs-table-active-bg:rgb(55.2, 58.8, 62.4);--bs-table-active-color:#fff;--bs-table-hover-bg:rgb(49.65, 53.35, 57.05);--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.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 + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-inset);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:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:rgb(134,182.5,254);outline:0;box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);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:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline: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 + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img: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 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-inset);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:rgb(134,182.5,254);outline:0;box-shadow:var(--bs-box-shadow-inset),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:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.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-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;print-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:rgb(134,182.5,254);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]{--bs-form-check-bg-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 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-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;--bs-form-check-bg-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{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: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");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);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{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgb%28134, 182.5, 254%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: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-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.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}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.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;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-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}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:rgb(182.4,211.5,254.4)}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem;box-shadow:var(--bs-box-shadow-inset)}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-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}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:rgb(182.4,211.5,254.4)}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem;box-shadow:var(--bs-box-shadow-inset)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem .75rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) 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,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder),.form-floating>.form-control:not(:-moz-placeholder){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:.75rem}.form-floating>.form-control:not(:-moz-placeholder)~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:not(:-moz-placeholder)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:var(--bs-secondary-bg)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.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:var(--bs-border-radius-lg)}.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:var(--bs-border-radius-sm)}.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)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){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>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){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:calc(-1 * var(--bs-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.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:var(--bs-success);border-radius:var(--bs-border-radius)}.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:var(--bs-form-valid-border-color);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.73.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.1'/%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:var(--bs-form-valid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-success-rgb),.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:var(--bs-form-valid-border-color)}.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"]{--bs-form-select-bg-icon: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.73.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.1'/%3e%3c/svg%3e");padding-right:4.125rem;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:var(--bs-form-valid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.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:var(--bs-danger);border-radius:var(--bs-border-radius)}.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:var(--bs-form-invalid-border-color);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:var(--bs-form-invalid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-danger-rgb),.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:var(--bs-form-invalid-border-color)}.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"]{--bs-form-select-bg-icon: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");padding-right:4.125rem;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:var(--bs-form-invalid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);box-shadow:var(--bs-btn-box-shadow);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:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-box-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-box-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color);box-shadow:var(--bs-btn-active-shadow)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-active-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-active-shadow),var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity);box-shadow:none}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(11.05, 93.5, 215.05);--bs-btn-hover-border-color:rgb(10.4, 88, 202.4);--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(10.4, 88, 202.4);--bs-btn-active-border-color:rgb(9.75, 82.5, 189.75);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(91.8, 99.45, 106.25);--bs-btn-hover-border-color:rgb(86.4, 93.6, 100);--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(86.4, 93.6, 100);--bs-btn-active-border-color:rgb(81, 87.75, 93.75);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(21.25, 114.75, 71.4);--bs-btn-hover-border-color:rgb(20, 108, 67.2);--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(20, 108, 67.2);--bs-btn-active-border-color:rgb(18.75, 101.25, 63);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgb(49.3, 209.95, 242.25);--bs-btn-hover-border-color:rgb(37.2, 207.3, 241.5);--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:rgb(61.4, 212.6, 243);--bs-btn-active-border-color:rgb(37.2, 207.3, 241.5);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgb(255, 202.3, 44.2);--bs-btn-hover-border-color:rgb(255, 199.2, 31.8);--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:rgb(255, 205.4, 56.6);--bs-btn-active-border-color:rgb(255, 199.2, 31.8);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(187, 45.05, 58.65);--bs-btn-hover-border-color:rgb(176, 42.4, 55.2);--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(176, 42.4, 55.2);--bs-btn-active-border-color:rgb(165, 39.75, 51.75);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgb(210.8, 211.65, 212.5);--bs-btn-hover-border-color:rgb(198.4, 199.2, 200);--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:rgb(198.4, 199.2, 200);--bs-btn-active-border-color:rgb(186, 186.75, 187.5);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(66.3, 69.7, 73.1);--bs-btn-hover-border-color:rgb(55.2, 58.8, 62.4);--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(77.4, 80.6, 83.8);--bs-btn-active-border-color:rgb(55.2, 58.8, 62.4);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.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}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{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{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius);box-shadow:var(--bs-dropdown-box-shadow)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-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{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.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.show .dropdown-toggle{box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.show .dropdown-toggle.btn-link{box-shadow:none}.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:calc(-1 * var(--bs-border-width))}.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:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;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:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;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{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:1rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.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:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@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 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg: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")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: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")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius);box-shadow:var(--bs-card-box-shadow)}.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:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@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{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='rgb%285.2, 44, 101.2%29' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.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-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgb%28109.8, 168, 253.8%29'%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-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgb%28109.8, 168, 253.8%29'%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-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);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:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item .page-link{border-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius);box-shadow:var(--bs-progress-box-shadow)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@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:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.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:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg: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 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@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% - var(--bs-modal-margin) * 2)}.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% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);box-shadow:var(--bs-modal-box-shadow);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-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-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@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-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);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;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);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;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius);box-shadow:var(--bs-popover-box-shadow)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.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:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.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%;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;filter:var(--bs-carousel-control-icon-filter);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 0'/%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-.708'/%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%}.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:var(--bs-carousel-indicator-active-bg);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:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;flex-shrink:0;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.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:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.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:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){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}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!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}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.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}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y: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-inline-grid{display:inline-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:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{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:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!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}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.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}.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}.m-n1{margin:-.25rem!important}.m-n2{margin:-.5rem!important}.m-n3{margin:-1rem!important}.m-n4{margin:-1.5rem!important}.m-n5{margin:-3rem!important}.mx-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-n1{margin-top:-.25rem!important}.mt-n2{margin-top:-.5rem!important}.mt-n3{margin-top:-1rem!important}.mt-n4{margin-top:-1.5rem!important}.mt-n5{margin-top:-3rem!important}.me-n1{margin-right:-.25rem!important}.me-n2{margin-right:-.5rem!important}.me-n3{margin-right:-1rem!important}.me-n4{margin-right:-1.5rem!important}.me-n5{margin-right:-3rem!important}.mb-n1{margin-bottom:-.25rem!important}.mb-n2{margin-bottom:-.5rem!important}.mb-n3{margin-bottom:-1rem!important}.mb-n4{margin-bottom:-1.5rem!important}.mb-n5{margin-bottom:-3rem!important}.ms-n1{margin-left:-.25rem!important}.ms-n2{margin-left:-.5rem!important}.ms-n3{margin-left:-1rem!important}.ms-n4{margin-left:-1.5rem!important}.ms-n5{margin-left:-3rem!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}.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}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap: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}.fs-7{font-size:.875rem!important}.fs-8{font-size:.75rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!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{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!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:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-sm-n1{margin:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.m-sm-n3{margin:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mx-sm-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-sm-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-sm-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-sm-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-sm-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-sm-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-sm-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-sm-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-sm-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-sm-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-sm-n1{margin-top:-.25rem!important}.mt-sm-n2{margin-top:-.5rem!important}.mt-sm-n3{margin-top:-1rem!important}.mt-sm-n4{margin-top:-1.5rem!important}.mt-sm-n5{margin-top:-3rem!important}.me-sm-n1{margin-right:-.25rem!important}.me-sm-n2{margin-right:-.5rem!important}.me-sm-n3{margin-right:-1rem!important}.me-sm-n4{margin-right:-1.5rem!important}.me-sm-n5{margin-right:-3rem!important}.mb-sm-n1{margin-bottom:-.25rem!important}.mb-sm-n2{margin-bottom:-.5rem!important}.mb-sm-n3{margin-bottom:-1rem!important}.mb-sm-n4{margin-bottom:-1.5rem!important}.mb-sm-n5{margin-bottom:-3rem!important}.ms-sm-n1{margin-left:-.25rem!important}.ms-sm-n2{margin-left:-.5rem!important}.ms-sm-n3{margin-left:-1rem!important}.ms-sm-n4{margin-left:-1.5rem!important}.ms-sm-n5{margin-left:-3rem!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}.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}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-md-n1{margin:-.25rem!important}.m-md-n2{margin:-.5rem!important}.m-md-n3{margin:-1rem!important}.m-md-n4{margin:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mx-md-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-md-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-md-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-md-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-md-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-md-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-md-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-md-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-md-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-md-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-md-n1{margin-top:-.25rem!important}.mt-md-n2{margin-top:-.5rem!important}.mt-md-n3{margin-top:-1rem!important}.mt-md-n4{margin-top:-1.5rem!important}.mt-md-n5{margin-top:-3rem!important}.me-md-n1{margin-right:-.25rem!important}.me-md-n2{margin-right:-.5rem!important}.me-md-n3{margin-right:-1rem!important}.me-md-n4{margin-right:-1.5rem!important}.me-md-n5{margin-right:-3rem!important}.mb-md-n1{margin-bottom:-.25rem!important}.mb-md-n2{margin-bottom:-.5rem!important}.mb-md-n3{margin-bottom:-1rem!important}.mb-md-n4{margin-bottom:-1.5rem!important}.mb-md-n5{margin-bottom:-3rem!important}.ms-md-n1{margin-left:-.25rem!important}.ms-md-n2{margin-left:-.5rem!important}.ms-md-n3{margin-left:-1rem!important}.ms-md-n4{margin-left:-1.5rem!important}.ms-md-n5{margin-left:-3rem!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}.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}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-lg-n1{margin:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.m-lg-n3{margin:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mx-lg-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-lg-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-lg-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-lg-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-lg-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-lg-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-lg-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-lg-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-lg-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-lg-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-lg-n1{margin-top:-.25rem!important}.mt-lg-n2{margin-top:-.5rem!important}.mt-lg-n3{margin-top:-1rem!important}.mt-lg-n4{margin-top:-1.5rem!important}.mt-lg-n5{margin-top:-3rem!important}.me-lg-n1{margin-right:-.25rem!important}.me-lg-n2{margin-right:-.5rem!important}.me-lg-n3{margin-right:-1rem!important}.me-lg-n4{margin-right:-1.5rem!important}.me-lg-n5{margin-right:-3rem!important}.mb-lg-n1{margin-bottom:-.25rem!important}.mb-lg-n2{margin-bottom:-.5rem!important}.mb-lg-n3{margin-bottom:-1rem!important}.mb-lg-n4{margin-bottom:-1.5rem!important}.mb-lg-n5{margin-bottom:-3rem!important}.ms-lg-n1{margin-left:-.25rem!important}.ms-lg-n2{margin-left:-.5rem!important}.ms-lg-n3{margin-left:-1rem!important}.ms-lg-n4{margin-left:-1.5rem!important}.ms-lg-n5{margin-left:-3rem!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}.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}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-xl-n1{margin:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.m-xl-n3{margin:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mx-xl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xl-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xl-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xl-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-xl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xl-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xl-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xl-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-xl-n1{margin-top:-.25rem!important}.mt-xl-n2{margin-top:-.5rem!important}.mt-xl-n3{margin-top:-1rem!important}.mt-xl-n4{margin-top:-1.5rem!important}.mt-xl-n5{margin-top:-3rem!important}.me-xl-n1{margin-right:-.25rem!important}.me-xl-n2{margin-right:-.5rem!important}.me-xl-n3{margin-right:-1rem!important}.me-xl-n4{margin-right:-1.5rem!important}.me-xl-n5{margin-right:-3rem!important}.mb-xl-n1{margin-bottom:-.25rem!important}.mb-xl-n2{margin-bottom:-.5rem!important}.mb-xl-n3{margin-bottom:-1rem!important}.mb-xl-n4{margin-bottom:-1.5rem!important}.mb-xl-n5{margin-bottom:-3rem!important}.ms-xl-n1{margin-left:-.25rem!important}.ms-xl-n2{margin-left:-.5rem!important}.ms-xl-n3{margin-left:-1rem!important}.ms-xl-n4{margin-left:-1.5rem!important}.ms-xl-n5{margin-left:-3rem!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}.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}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-xxl-n1{margin:-.25rem!important}.m-xxl-n2{margin:-.5rem!important}.m-xxl-n3{margin:-1rem!important}.m-xxl-n4{margin:-1.5rem!important}.m-xxl-n5{margin:-3rem!important}.mx-xxl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xxl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xxl-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xxl-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xxl-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-xxl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xxl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xxl-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xxl-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xxl-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-xxl-n1{margin-top:-.25rem!important}.mt-xxl-n2{margin-top:-.5rem!important}.mt-xxl-n3{margin-top:-1rem!important}.mt-xxl-n4{margin-top:-1.5rem!important}.mt-xxl-n5{margin-top:-3rem!important}.me-xxl-n1{margin-right:-.25rem!important}.me-xxl-n2{margin-right:-.5rem!important}.me-xxl-n3{margin-right:-1rem!important}.me-xxl-n4{margin-right:-1.5rem!important}.me-xxl-n5{margin-right:-3rem!important}.mb-xxl-n1{margin-bottom:-.25rem!important}.mb-xxl-n2{margin-bottom:-.5rem!important}.mb-xxl-n3{margin-bottom:-1rem!important}.mb-xxl-n4{margin-bottom:-1.5rem!important}.mb-xxl-n5{margin-bottom:-3rem!important}.ms-xxl-n1{margin-left:-.25rem!important}.ms-xxl-n2{margin-left:-.5rem!important}.ms-xxl-n3{margin-left:-1rem!important}.ms-xxl-n4{margin-left:-1.5rem!important}.ms-xxl-n5{margin-left:-3rem!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}.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}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap: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-inline-grid{display:inline-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}}@keyframes flipInX{0%{opacity:0;transition-timing-function:ease-in;transform:perspective(400px) rotate3d(1,0,0,90deg)}40%{transition-timing-function:ease-in;transform:perspective(400px) rotate3d(1,0,0,-20deg)}60%{opacity:1;transform:perspective(400px) rotate3d(1,0,0,10deg)}80%{transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{transform:perspective(400px)}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}@keyframes shake{0%{transform:translate(2px,1px) rotate(0)}10%{transform:translate(-1px,-2px) rotate(-2deg)}20%{transform:translate(-3px,0) rotate(3deg)}30%{transform:translate(0,2px) rotate(0)}40%{transform:translate(1px,-1px) rotate(1deg)}50%{transform:translate(-1px,2px) rotate(-1deg)}60%{transform:translate(-3px,1px) rotate(0)}70%{transform:translate(2px,1px) rotate(-2deg)}80%{transform:translate(-1px,-1px) rotate(4deg)}90%{transform:translate(2px,2px) rotate(0)}100%{transform:translate(1px,-2px) rotate(-1deg)}}@keyframes wobble{0%{transform:none}15%{transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{transform:none}}:root,[data-bs-theme=light]{--lte-sidebar-width:250px}.app-wrapper{position:relative;display:grid;grid-template-areas:"lte-app-sidebar lte-app-header" "lte-app-sidebar lte-app-main" "lte-app-sidebar lte-app-footer";grid-template-rows:min-content 1fr min-content;grid-template-columns:auto 1fr;grid-gap:0;align-content:stretch;align-items:stretch;max-width:100vw;min-height:100vh}.app-wrapper>*{min-width:0}.app-content{padding:0 .5rem}.app-header{z-index:1034;grid-area:lte-app-header;max-width:100vw;border-bottom:1px solid var(--bs-border-color);transition:.3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-header{transition:none}}.app-header .nav-link{position:relative;height:2.5rem}.navbar-badge{position:absolute;top:9px;right:5px;padding:2px 4px;font-size:.6rem;font-weight:400}.fixed-header .app-header{position:sticky;top:0;z-index:1030}.app-sidebar{--lte-sidebar-hover-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-color:#343a40;--lte-sidebar-hover-color:#212529;--lte-sidebar-active-color:#000;--lte-sidebar-menu-active-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-menu-active-color:#000;--lte-sidebar-submenu-bg:transparent;--lte-sidebar-submenu-color:#777;--lte-sidebar-submenu-hover-color:#000;--lte-sidebar-submenu-hover-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-submenu-active-color:#212529;--lte-sidebar-submenu-active-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-header-color:rgb(49.4, 55.1, 60.8);z-index:1038;grid-area:lte-app-sidebar;min-width:var(--lte-sidebar-width);max-width:var(--lte-sidebar-width);transition:min-width .3s ease-in-out,max-width .3s ease-in-out,margin-left .3s ease-in-out,margin-right .3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-sidebar{transition:none}}.sidebar-brand{display:flex;align-items:center;justify-content:center;height:3.5rem;padding:.8125rem .5rem;overflow:hidden;font-size:1.25rem;white-space:nowrap;border-bottom:1px solid var(--bs-border-color);transition:width .3s ease-in-out}@media (prefers-reduced-motion:reduce){.sidebar-brand{transition:none}}.sidebar-brand .brand-link{display:flex;align-items:center;text-decoration:none}.sidebar-brand .brand-link .brand-image{float:left;width:auto;max-height:33px;line-height:.8}.sidebar-brand .brand-link .brand-image-xs{float:left;width:auto;max-height:33px;margin-top:-.1rem;line-height:.8}.sidebar-brand .brand-link .brand-image-xl{width:auto;max-height:40px;line-height:.8}.sidebar-brand .brand-link .brand-image-xl.single{margin-top:-.3rem}.sidebar-brand .brand-text{margin-left:.5rem;color:rgba(var(--bs-emphasis-color-rgb),.8);transition:flex .3s ease-in-out,width .3s ease-in-out}@media (prefers-reduced-motion:reduce){.sidebar-brand .brand-text{transition:none}}.sidebar-brand .brand-text:hover{color:var(--bs-emphasis-color)}.sidebar-wrapper{padding-top:.5rem;padding-right:.5rem;padding-bottom:.5rem;padding-left:.5rem;scrollbar-color:var(--bs-secondary-bg) transparent}.sidebar-wrapper::-webkit-scrollbar-thumb{background-color:var(--bs-secondary-bg)}.sidebar-wrapper::-webkit-scrollbar-track{background-color:transparent}.sidebar-wrapper::-webkit-scrollbar-corner{background-color:transparent}.sidebar-wrapper{scrollbar-width:thin}.sidebar-wrapper::-webkit-scrollbar{width:.5rem;height:.5rem}.sidebar-wrapper .nav-item{max-width:100%}.sidebar-wrapper .nav-link{display:flex;justify-content:flex-start}.sidebar-wrapper .nav-link p{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-wrapper .nav-icon{display:flex;align-items:center;justify-content:center;min-width:1.5rem;max-width:1.5rem}.sidebar-wrapper .sidebar-menu>.nav-item.menu-open .nav-link.active:not(:hover){--lte-sidebar-menu-active-bg:transparent}.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link:active,.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link:focus{color:var(--lte-sidebar-color)}.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link.active:not(:hover){color:var(--lte-sidebar-menu-active-color);background-color:var(--lte-sidebar-menu-active-bg)}.sidebar-wrapper .sidebar-menu>.nav-item.menu-open>.nav-link,.sidebar-wrapper .sidebar-menu>.nav-item:hover>.nav-link,.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link:focus{color:var(--lte-sidebar-hover-color);background-color:var(--lte-sidebar-hover-bg)}.sidebar-wrapper .sidebar-menu>.nav-item>.nav-treeview{background-color:var(--lte-sidebar-submenu-bg)}.sidebar-wrapper .nav-header{color:var(--lte-sidebar-header-color);background-color:inherit}.sidebar-wrapper a{color:var(--lte-sidebar-color)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link{color:var(--lte-sidebar-submenu-color)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link:focus,.sidebar-wrapper .nav-treeview>.nav-item>.nav-link:hover{color:var(--lte-sidebar-submenu-hover-color)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link.active,.sidebar-wrapper .nav-treeview>.nav-item>.nav-link.active:focus,.sidebar-wrapper .nav-treeview>.nav-item>.nav-link.active:hover{color:var(--lte-sidebar-submenu-active-color);background-color:var(--lte-sidebar-submenu-active-bg)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link:hover{background-color:var(--lte-sidebar-submenu-hover-bg)}.sidebar-menu .nav-item>.nav-link{margin-bottom:.2rem}.sidebar-menu .nav-item>.nav-link .nav-arrow{transition:transform ease-in-out .3s}@media (prefers-reduced-motion:reduce){.sidebar-menu .nav-item>.nav-link .nav-arrow{transition:none}}.sidebar-menu .nav-item>.nav-link .nav-arrow{transform:translateY(-50%);animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-menu .nav-link>.nav-badge,.sidebar-menu .nav-link>p>.nav-badge{position:absolute;top:50%;right:1rem;transform:translateY(-50%)}.sidebar-menu .nav-link>.nav-arrow,.sidebar-menu .nav-link>p>.nav-arrow{position:absolute;top:50%;right:1rem}.sidebar-menu .nav-link{position:relative;width:100%;transition:width ease-in-out .3s}@media (prefers-reduced-motion:reduce){.sidebar-menu .nav-link{transition:none}}.sidebar-menu .nav-link{border-radius:.375rem}.sidebar-menu .nav-link p{display:inline;padding-left:.5rem;margin:0}.sidebar-menu .nav-header{padding:.5rem .75rem;font-size:.9rem}.sidebar-menu .nav-treeview{display:none;padding:0;list-style:none}.nav-indent .sidebar-menu .nav-treeview{padding-left:.5rem}.sidebar-menu .menu-open>.nav-treeview{display:block}.sidebar-menu .menu-open>.nav-link .nav-arrow{transform:translateY(-50%) rotate(90deg)}.sidebar-menu .nav-link>.nav-arrow,.sidebar-menu .nav-link>.nav-badge,.sidebar-menu .nav-link>p>.nav-arrow,.sidebar-menu .nav-link>p>.nav-badge{right:1rem!important;left:auto!important}.nav-compact.nav-indent .nav-treeview{padding-left:0}.nav-compact.nav-indent .nav-treeview .nav-item{padding-left:.5rem}.sidebar-mini.sidebar-collapse.nav-indent .app-sidebar:hover .nav-treeview{padding-left:0}.sidebar-mini.sidebar-collapse.nav-indent .app-sidebar:hover .nav-treeview .nav-item{padding-left:.5rem}.sidebar-collapse.nav-compact.nav-indent .nav-treeview .nav-item{padding-left:0}.nav-compact .nav-link{border-radius:0;margin-bottom:0!important}.sidebar-menu,.sidebar-menu .nav-link,.sidebar-menu>.nav-header{white-space:nowrap}.logo-xl,.logo-xs{position:absolute;visibility:visible;opacity:1}.logo-xl.brand-image-xs,.logo-xs.brand-image-xs{top:12px;left:18px}.logo-xl.brand-image-xl,.logo-xs.brand-image-xl{top:6px;left:12px}.logo-xs{visibility:hidden;opacity:0}.logo-xs.brand-image-xl{top:8px;left:16px}.brand-link.logo-switch::before{content:" "}.sidebar-mini.sidebar-collapse .app-sidebar{min-width:4.6rem;max-width:4.6rem}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-header{display:none}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link{width:3.6rem}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link p{display:inline-block;width:0;white-space:nowrap}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-arrow,.sidebar-mini.sidebar-collapse .sidebar-menu .nav-badge{display:none;animation-name:fadeOut;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .brand-text{display:inline-block;max-width:0;overflow:hidden}.sidebar-mini.sidebar-collapse .brand-text,.sidebar-mini.sidebar-collapse .logo-xl,.sidebar-mini.sidebar-collapse .nav-arrow,.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link p{visibility:hidden;animation-name:fadeOut;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .logo-xs{display:inline-block;visibility:visible;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .app-sidebar:hover{min-width:var(--lte-sidebar-width);max-width:var(--lte-sidebar-width)}.sidebar-mini.sidebar-collapse .app-sidebar:hover .sidebar-menu .nav-header{display:inline-block}.sidebar-mini.sidebar-collapse .app-sidebar:hover .sidebar-menu .nav-link{width:auto}.sidebar-mini.sidebar-collapse .app-sidebar:hover .brand-text,.sidebar-mini.sidebar-collapse .app-sidebar:hover .logo-xl,.sidebar-mini.sidebar-collapse .app-sidebar:hover .sidebar-menu .nav-link p{width:auto;margin-left:0;visibility:visible;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .app-sidebar:hover .brand-text{display:inline;max-width:inherit;margin-left:.5rem;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .app-sidebar:hover .nav-arrow,.sidebar-mini.sidebar-collapse .app-sidebar:hover .nav-badge{display:inline-block;visibility:visible;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-mini.sidebar-collapse .app-sidebar:hover .nav-link p{padding-left:.5rem}.sidebar-mini.sidebar-collapse .app-sidebar:hover .logo-xs{visibility:hidden;animation-name:fadeOut;animation-duration:.3s;animation-fill-mode:both}.sidebar-collapse:not(.sidebar-mini) .app-sidebar{margin-left:calc(var(--lte-sidebar-width) * -1)}@media (min-width:576px){.sidebar-expand-sm.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-sm.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-sm.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-sm.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-sm.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-sm.sidebar-open .nav-link>.nav-badge,.sidebar-expand-sm.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-sm.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-sm.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:575.98px){.sidebar-expand-sm::before{display:none;content:"575.98px"}.sidebar-expand-sm .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-sm .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-sm.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-sm.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:768px){.sidebar-expand-md.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-md.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-md.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-md.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-md.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-md.sidebar-open .nav-link>.nav-badge,.sidebar-expand-md.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-md.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-md.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:767.98px){.sidebar-expand-md::before{display:none;content:"767.98px"}.sidebar-expand-md .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-md .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-md.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-md.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:992px){.sidebar-expand-lg.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-lg.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-lg.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-lg.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-lg.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-lg.sidebar-open .nav-link>.nav-badge,.sidebar-expand-lg.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-lg.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-lg.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:991.98px){.sidebar-expand-lg::before{display:none;content:"991.98px"}.sidebar-expand-lg .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-lg .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-lg.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-lg.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:1200px){.sidebar-expand-xl.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-xl.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-xl.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-xl.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-xl.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xl.sidebar-open .nav-link>.nav-badge,.sidebar-expand-xl.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-xl.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-xl.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:1199.98px){.sidebar-expand-xl::before{display:none;content:"1199.98px"}.sidebar-expand-xl .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-xl .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xl.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-xl.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:1400px){.sidebar-expand-xxl.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-xxl.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-xxl.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-xxl.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-xxl.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xxl.sidebar-open .nav-link>.nav-badge,.sidebar-expand-xxl.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-xxl.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-xxl.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:1399.98px){.sidebar-expand-xxl::before{display:none;content:"1399.98px"}.sidebar-expand-xxl .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-xxl .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xxl.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-xxl.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}.sidebar-expand.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand.sidebar-open .nav-link>.nav-badge,.sidebar-expand.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand.sidebar-open .nav-link>.nav-arrow,.sidebar-expand.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand::before{display:none;content:""}.sidebar-expand .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}.app-sidebar .brand-text,.app-sidebar .logo-xl,.app-sidebar .logo-xs,.sidebar-menu .nav-link p{transition:margin-left .3s linear,opacity .3s ease,visibility .3s ease}@media (prefers-reduced-motion:reduce){.app-sidebar .brand-text,.app-sidebar .logo-xl,.app-sidebar .logo-xs,.sidebar-menu .nav-link p{transition:none}}.app-loaded.sidebar-mini.sidebar-collapse .brand-text,.app-loaded.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link p{animation-duration:.3s}body:not(.app-loaded) .app-footer,body:not(.app-loaded) .app-header,body:not(.app-loaded) .app-main,body:not(.app-loaded) .app-sidebar{transition:none!important}@media (prefers-reduced-motion:reduce){body:not(.app-loaded) .app-footer,body:not(.app-loaded) .app-header,body:not(.app-loaded) .app-main,body:not(.app-loaded) .app-sidebar{transition:none}}body:not(.app-loaded) .app-footer,body:not(.app-loaded) .app-header,body:not(.app-loaded) .app-main,body:not(.app-loaded) .app-sidebar{animation-duration:0s!important}.hold-transition .app-footer,.hold-transition .app-header,.hold-transition .app-main,.hold-transition .app-sidebar,.hold-transition .nav-arrow,.hold-transition .nav-badge{transition:none!important}@media (prefers-reduced-motion:reduce){.hold-transition .app-footer,.hold-transition .app-header,.hold-transition .app-main,.hold-transition .app-sidebar,.hold-transition .nav-arrow,.hold-transition .nav-badge{transition:none}}.hold-transition .app-footer,.hold-transition .app-header,.hold-transition .app-main,.hold-transition .app-sidebar,.hold-transition .nav-arrow,.hold-transition .nav-badge{animation-duration:0s!important}[data-bs-theme=dark] .app-sidebar,[data-bs-theme=dark].app-sidebar{--lte-sidebar-hover-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-color:#c2c7d0;--lte-sidebar-hover-color:#fff;--lte-sidebar-active-color:#fff;--lte-sidebar-menu-active-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-menu-active-color:#fff;--lte-sidebar-submenu-bg:transparent;--lte-sidebar-submenu-color:#c2c7d0;--lte-sidebar-submenu-hover-color:#fff;--lte-sidebar-submenu-hover-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-submenu-active-color:#fff;--lte-sidebar-submenu-active-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-header-color:rgb(197.05, 201.8, 210.35)}.app-main{position:relative;display:flex;flex-direction:column;grid-area:lte-app-main;max-width:100vw;padding-bottom:.75rem;transition:.3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-main{transition:none}}.app-main .app-content-header{padding:1rem .5rem}.app-main .app-content-header .breadcrumb{padding:0;margin-bottom:0;line-height:2.5rem}.app-main .app-content-header .breadcrumb a{text-decoration:none}.app-main .app-content-bottom-area,.app-main .app-content-top-area{color:var(--bs-secondary-color);background-color:var(--bs-body-bg)}.app-main .app-content-top-area{padding:1rem 0;border-bottom:1px solid var(--bs-border-color)}.app-main .app-content-bottom-area{padding:1rem 0;margin-top:auto;margin-bottom:-.75rem;border-top:1px solid var(--bs-border-color)}.app-footer{grid-area:lte-app-footer;width:inherit;max-width:100vw;min-height:3rem;padding:1rem;color:var(--bs-secondary-color);background-color:var(--bs-body-bg);border-top:1px solid var(--bs-border-color);transition:.3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-footer{transition:none}}.fixed-footer .app-footer{position:sticky;bottom:0;z-index:1030}.fs-7 .dropdown-menu{font-size:.875rem!important}.fs-7 .dropdown-toggle::after{vertical-align:.2rem}.dropdown-item-title{margin:0;font-size:1rem}.dropdown-icon::after{margin-left:0}.dropdown-menu-lg{min-width:280px;max-width:300px;padding:0}.dropdown-menu-lg .dropdown-divider{margin:0}.dropdown-menu-lg .dropdown-item{padding:.5rem 1rem}.dropdown-menu-lg p{margin:0;word-wrap:break-word;white-space:normal}.dropdown-submenu{position:relative}.dropdown-submenu>a::after{border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid;float:right;margin-top:.5rem;margin-left:.5rem}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:0;margin-left:0}.dropdown-hover .dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.nav-item.dropdown:hover>.dropdown-menu,.dropdown-hover:hover>.dropdown-menu{display:block}.dropdown-menu-xl{min-width:360px;max-width:420px;padding:0}.dropdown-menu-xl .dropdown-divider{margin:0}.dropdown-menu-xl .dropdown-item{padding:.5rem 1rem}.dropdown-menu-xl p{margin:0;word-wrap:break-word;white-space:normal}.dropdown-footer,.dropdown-header{display:block;padding:.5rem 1rem;font-size:.875rem;text-align:center}.open:not(.dropup)>.animated-dropdown-menu{animation:flipInX .7s both;backface-visibility:visible!important}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:575.98px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;background-color:var(--bs-body-bg);border:1px solid var(--bs-border-color)}}.navbar-nav>.user-menu>.nav-link::after{content:none}.navbar-nav>.user-menu>.dropdown-menu{border-top-left-radius:0;border-top-right-radius:0;width:280px;padding:0}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{min-height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;width:90px;height:90px;border:3px solid;border-color:transparent;border-color:var(--bs-border-color-translucent)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;margin-top:10px;font-size:17px;word-wrap:break-word}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>.small,.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body::after{display:block;clear:both;content:""}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-top:1px solid var(--bs-border-color);border-bottom:1px solid var(--bs-border-color-translucent)}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{text-decoration:none}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:var(--bs-body-color)!important;background-color:var(--bs-body-bg)!important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer::after{display:block;clear:both;content:""}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{padding:10px;background-color:var(--bs-light-bg)}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:var(--bs-body-color)}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:var(--bs-body-bg)}}@media (min-width:576px){.navbar-nav>.user-menu .user-image{float:none;margin-top:-8px;margin-right:.4rem;line-height:10px}}.navbar-nav>.user-menu .user-image{float:left;width:2rem;height:2rem;margin-top:-2px;border-radius:50%}.callout{--bs-link-color-rgb:var(--lte-callout-link);--bs-code-color:var(--lte-callout-code-color);padding:1.25rem;color:var(--lte-callout-color,inherit);background-color:var(--lte-callout-bg,var(--bs-gray-100));border-left:.25rem solid var(--lte-callout-border,var(--bs-gray-300))}.callout .callout-link{font-weight:700;color:var(--bs-callout-link-color)}.callout .h4,.callout h4{margin-bottom:.25rem}.callout>:last-child{margin-bottom:0}.callout+.callout{margin-top:-.25rem}.callout-primary{--lte-callout-color:var(--bs-primary-text-emphasis);--lte-callout-bg:var(--bs-primary-bg-subtle);--lte-callout-border:var(--bs-primary-border-subtle);--bs-callout-link-color:var(--bs-primary-text-emphasis)}.callout-secondary{--lte-callout-color:var(--bs-secondary-text-emphasis);--lte-callout-bg:var(--bs-secondary-bg-subtle);--lte-callout-border:var(--bs-secondary-border-subtle);--bs-callout-link-color:var(--bs-secondary-text-emphasis)}.callout-success{--lte-callout-color:var(--bs-success-text-emphasis);--lte-callout-bg:var(--bs-success-bg-subtle);--lte-callout-border:var(--bs-success-border-subtle);--bs-callout-link-color:var(--bs-success-text-emphasis)}.callout-info{--lte-callout-color:var(--bs-info-text-emphasis);--lte-callout-bg:var(--bs-info-bg-subtle);--lte-callout-border:var(--bs-info-border-subtle);--bs-callout-link-color:var(--bs-info-text-emphasis)}.callout-warning{--lte-callout-color:var(--bs-warning-text-emphasis);--lte-callout-bg:var(--bs-warning-bg-subtle);--lte-callout-border:var(--bs-warning-border-subtle);--bs-callout-link-color:var(--bs-warning-text-emphasis)}.callout-danger{--lte-callout-color:var(--bs-danger-text-emphasis);--lte-callout-bg:var(--bs-danger-bg-subtle);--lte-callout-border:var(--bs-danger-border-subtle);--bs-callout-link-color:var(--bs-danger-text-emphasis)}.callout-light{--lte-callout-color:var(--bs-light-text-emphasis);--lte-callout-bg:var(--bs-light-bg-subtle);--lte-callout-border:var(--bs-light-border-subtle);--bs-callout-link-color:var(--bs-light-text-emphasis)}.callout-dark{--lte-callout-color:var(--bs-dark-text-emphasis);--lte-callout-bg:var(--bs-dark-bg-subtle);--lte-callout-border:var(--bs-dark-border-subtle);--bs-callout-link-color:var(--bs-dark-text-emphasis)}.compact-mode .app-header{max-height:2.75rem}.compact-mode .app-header .nav-link{max-height:1.75rem}.compact-mode .nav-link{--bs-nav-link-padding-y:.25rem;--bs-nav-link-padding-x:.5rem}.compact-mode.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover){min-width:3.1rem;max-width:3.1rem}.compact-mode.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-menu .nav-link{width:2.1rem!important}.compact-mode .logo-xl,.compact-mode .logo-xs{max-height:2.75rem}.compact-mode .brand-image{width:1.75rem;height:1.75rem}.compact-mode .sidebar-brand{height:2.75rem}.compact-mode .app-footer{padding:.5rem}.compact-mode .sidebar-wrapper .nav-icon{min-width:1.1rem;max-width:1.1rem}.astro-code{padding:.75rem;border-radius:.375rem}.progress{border-radius:1px}.progress.vertical{position:relative;display:inline-block;width:30px;height:200px;margin-right:10px}.progress.vertical>.progress-bar{position:absolute;bottom:0;width:100%}.progress.vertical.progress-sm,.progress.vertical.sm{width:20px}.progress.vertical.progress-xs,.progress.vertical.xs{width:10px}.progress.vertical.progress-xxs,.progress.vertical.xxs{width:3px}.progress-group{margin-bottom:.5rem}.progress-sm{height:10px}.progress-xs{height:7px}.progress-xxs{height:3px}.table tr>td .progress{margin:0}.card{box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2)}.card[class*=card-]:not(.card-outline)>.card-header,.card[class*=text-bg-]:not(.card-outline)>.card-header{color:var(--lte-card-variant-color);background-color:var(--lte-card-variant-bg)}.card[class*=card-]:not(.card-outline)>.card-header .btn-tool,.card[class*=text-bg-]:not(.card-outline)>.card-header .btn-tool{--bs-btn-color:rgba(var(--lte-card-variant-color-rgb), .8);--bs-btn-hover-color:var(--lte-card-variant-color)}.card.card-outline{border-top:3px solid var(--lte-card-variant-bg)}.card.maximized-card{position:fixed;top:0;left:0;z-index:1050;width:100%!important;max-width:100%!important;height:100%!important;max-height:100%!important}.card.maximized-card.was-collapsed .card-body{display:block!important}.card.maximized-card .card-body{overflow:auto}.card.maximized-card [data-lte-toggle=card-collapse]{display:none}.card.maximized-card [data-lte-icon=maximize]{display:none}.card.maximized-card .card-footer,.card.maximized-card .card-header{border-radius:0!important}.card:not(.maximized-card) [data-lte-icon=minimize]{display:none}.card.collapsed-card [data-lte-icon=collapse]{display:none}.card.collapsed-card .card-body,.card.collapsed-card .card-footer{display:none}.card:not(.collapsed-card) [data-lte-icon=expand]{display:none}.card .nav.flex-column>li{margin:0;border-bottom:1px solid var(--bs-border-color-translucent)}.card .nav.flex-column>li:last-of-type{border-bottom:0}.card.height-control .card-body{max-height:300px;overflow:auto}.card .border-end{border-right:1px solid var(--bs-border-color-translucent)}.card .border-start{border-left:1px solid var(--bs-border-color-translucent)}.card.card-tabs:not(.card-outline)>.card-header{border-bottom:0}.card.card-tabs:not(.card-outline)>.card-header .nav-item:first-child .nav-link{border-left-color:transparent}.card.card-tabs.card-outline .nav-item{border-bottom:0}.card.card-tabs.card-outline .nav-item:first-child .nav-link{margin-left:0;border-left:0}.card.card-tabs .card-tools{margin:.3rem .5rem}.card.card-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}.card.card-outline-tabs{border-top:0}.card.card-outline-tabs .card-header .nav-item:first-child .nav-link{margin-left:0;border-left:0}.card.card-outline-tabs .card-header a{text-decoration:none;border-top:3px solid transparent}.card.card-outline-tabs .card-header a:hover{border-top:3px solid var(--bs-border-color)}.card.card-outline-tabs .card-header a.active:hover{margin-top:0}.card.card-outline-tabs .card-tools{margin:.5rem .5rem .3rem}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-outline-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}html.maximized-card{overflow:hidden}.card-body::after,.card-footer::after,.card-header::after{display:block;clear:both;content:""}.card-header{position:relative;padding:1rem 1rem;background-color:transparent;border-bottom:1px solid var(--bs-border-color-translucent);border-top-left-radius:.375rem;border-top-right-radius:.375rem}.collapsed-card .card-header{border-bottom:0}.card-header>.card-tools{float:right;margin-right:-.5rem}.card-header>.card-tools .input-group,.card-header>.card-tools .nav,.card-header>.card-tools .pagination{margin-top:-.4rem;margin-bottom:-.4rem}.card-header>.card-tools [data-bs-toggle=tooltip]{position:relative}.card-title{float:left;margin:0;font-size:1.1rem;font-weight:400}.btn-tool{--bs-btn-padding-x:.5rem;--bs-btn-padding-y:.25rem}.btn-tool:not(.btn-tool-custom){--bs-btn-color:var(--bs-tertiary-color);--bs-btn-bg:transparent;--bs-btn-box-shadow:none;--bs-btn-hover-color:var(--bs-secondary-color);--bs-btn-active-border-color:transparent}.btn-tool{margin:-1rem 0;font-size:.875rem}.bg-primary,.card-primary,.text-bg-primary{--lte-card-variant-bg:#0d6efd;--lte-card-variant-bg-rgb:13,110,253;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-secondary,.card-secondary,.text-bg-secondary{--lte-card-variant-bg:#6c757d;--lte-card-variant-bg-rgb:108,117,125;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-success,.card-success,.text-bg-success{--lte-card-variant-bg:#198754;--lte-card-variant-bg-rgb:25,135,84;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-info,.card-info,.text-bg-info{--lte-card-variant-bg:#0dcaf0;--lte-card-variant-bg-rgb:13,202,240;--lte-card-variant-color:#000;--lte-card-variant-color-rgb:0,0,0}.bg-warning,.card-warning,.text-bg-warning{--lte-card-variant-bg:#ffc107;--lte-card-variant-bg-rgb:255,193,7;--lte-card-variant-color:#000;--lte-card-variant-color-rgb:0,0,0}.bg-danger,.card-danger,.text-bg-danger{--lte-card-variant-bg:#dc3545;--lte-card-variant-bg-rgb:220,53,69;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-light,.card-light,.text-bg-light{--lte-card-variant-bg:#f8f9fa;--lte-card-variant-bg-rgb:248,249,250;--lte-card-variant-color:#000;--lte-card-variant-color-rgb:0,0,0}.bg-dark,.card-dark,.text-bg-dark{--lte-card-variant-bg:#212529;--lte-card-variant-bg-rgb:33,37,41;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.card-body>.table{margin-bottom:0}.card-body>.table>thead>tr>td,.card-body>.table>thead>tr>th{border-top-width:0}.table:not(.table-dark){color:inherit}.table.table-head-fixed thead tr:first-child th{position:sticky;top:0;z-index:10;background-color:#fff;border-bottom:0;box-shadow:inset 0 1px 0 var(--bs-border-color),inset 0 -1px 0 var(--bs-border-color)}.table.no-border,.table.no-border td,.table.no-border th{border:0}.table.text-center,.table.text-center td,.table.text-center th{text-align:center}.table.table-valign-middle tbody>tr>td,.table.table-valign-middle tbody>tr>th,.table.table-valign-middle thead>tr>td,.table.table-valign-middle thead>tr>th{vertical-align:middle}.card-body.p-0 .table tbody>tr>td:first-of-type,.card-body.p-0 .table tbody>tr>th:first-of-type,.card-body.p-0 .table tfoot>tr>td:first-of-type,.card-body.p-0 .table tfoot>tr>th:first-of-type,.card-body.p-0 .table thead>tr>td:first-of-type,.card-body.p-0 .table thead>tr>th:first-of-type{padding-left:1.5rem}.card-body.p-0 .table tbody>tr>td:last-of-type,.card-body.p-0 .table tbody>tr>th:last-of-type,.card-body.p-0 .table tfoot>tr>td:last-of-type,.card-body.p-0 .table tfoot>tr>th:last-of-type,.card-body.p-0 .table thead>tr>td:last-of-type,.card-body.p-0 .table thead>tr>th:last-of-type{padding-right:1.5rem}.small-box{border-radius:.375rem;box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2);position:relative;display:block;margin-bottom:1.25rem;--bs-link-color-rgb:none;--bs-link-hover-color-rgb:none;--bs-heading-color:none}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;z-index:10;display:block;padding:3px 0;text-align:center;background-color:rgba(0,0,0,.07)}.small-box>.small-box-footer:hover{background-color:rgba(0,0,0,.1)}.small-box .h3,.small-box h3{font-size:calc(1.345rem + 1.14vw)}@media (min-width:1200px){.small-box .h3,.small-box h3{font-size:2.2rem}}.small-box .h3,.small-box h3{padding:0;margin:0 0 10px;font-weight:700;white-space:nowrap}@media (min-width:992px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:calc(1.285rem + .42vw)}}@media (min-width:992px) and (min-width:1200px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:1.6rem}}@media (min-width:992px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:calc(1.285rem + .42vw)}}@media (min-width:992px) and (min-width:1200px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:1.6rem}}@media (min-width:1200px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:calc(1.345rem + 1.14vw)}}@media (min-width:1200px) and (min-width:1200px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:2.2rem}}@media (min-width:1200px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:calc(1.345rem + 1.14vw)}}@media (min-width:1200px) and (min-width:1200px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:2.2rem}}.small-box p{font-size:1rem}.small-box p>.small,.small-box p>small{display:block;margin-top:5px;font-size:.9rem;color:#f8f9fa}.small-box .h3,.small-box h3,.small-box p{z-index:5}.small-box .small-box-icon{position:absolute;top:15px;right:15px;z-index:0;height:70px;font-size:70px;color:rgba(0,0,0,.15);transition:transform .3s linear}@media (prefers-reduced-motion:reduce){.small-box .small-box-icon{transition:none}}.small-box:hover .small-box-icon{transform:scale(1.1)}@media (max-width:575.98px){.small-box{text-align:center}.small-box .small-box-icon{display:none}.small-box p{font-size:12px}}.info-box{box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2);border-radius:.375rem;position:relative;display:flex;width:100%;min-height:80px;padding:.5rem;margin-bottom:1rem;color:var(--bs-body-color);background-color:var(--bs-body-bg)}.info-box .progress{height:2px;margin:5px 0;background-color:rgba(var(--lte-card-variant-color-rgb),.125)}.info-box .progress .progress-bar{background-color:var(--lte-card-variant-color)}.info-box .info-box-icon{display:flex;align-items:center;justify-content:center;width:70px;font-size:1.875rem;text-align:center;border-radius:.375rem}.info-box .info-box-icon>img{max-width:100%}.info-box .info-box-content{display:flex;flex:1;flex-direction:column;justify-content:center;padding:0 10px;line-height:1.8}.info-box .info-box-number{display:block;margin-top:.25rem;font-weight:700}.info-box .info-box-text,.info-box .progress-description{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.info-box .info-box-more{display:block}.info-box .progress-description{margin:0}@media (min-width:768px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{display:none}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{display:none}}@media (min-width:992px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:.75rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:.75rem;display:block}}@media (min-width:1200px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:1rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:1rem;display:block}}.timeline{position:relative;padding:0;margin:0 0 45px}.timeline::before{border-radius:.375rem;position:absolute;top:0;bottom:0;left:31px;width:4px;margin:0;content:"";background-color:var(--bs-border-color)}.timeline>div::after,.timeline>div::before{display:table;content:""}.timeline>div{position:relative;margin-right:10px;margin-bottom:15px}.timeline>div>.timeline-item{box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2);border-radius:.375rem;position:relative;padding:0;margin-top:0;margin-right:15px;margin-left:60px;color:var(--bs-body-color);background-color:var(--bs-body-bg)}.timeline>div>.timeline-item>.time{float:right;padding:10px;font-size:12px;color:var(--bs-secondary-color)}.timeline>div>.timeline-item>.timeline-header{padding:10px;margin:0;font-size:16px;line-height:1.1;color:var(--bs-secondary-color);border-bottom:1px solid var(--bs-border-color)}.timeline>div>.timeline-item>.timeline-header>a{font-weight:600;text-decoration:none}.timeline>div>.timeline-item>.timeline-body,.timeline>div>.timeline-item>.timeline-footer{padding:10px}.timeline>div>.timeline-item>.timeline-body>img{margin:10px}.timeline>div>.timeline-item>.timeline-body ol,.timeline>div>.timeline-item>.timeline-body ul,.timeline>div>.timeline-item>.timeline-body>dl{margin:0}.timeline>div .timeline-icon{position:absolute;top:0;left:18px;width:30px;height:30px;font-size:16px;line-height:30px;text-align:center;background-color:var(--bs-secondary-bg);border-radius:50%}.timeline>.time-label>span{border-radius:4px;display:inline-block;padding:5px;font-weight:600;background-color:var(--bs-body-bg)}.timeline-inverse>div>.timeline-item{box-shadow:none;background-color:var(--bs-tertiary-bg);border:1px solid var(--bs-border-color)}.timeline-inverse>div>.timeline-item>.timeline-header{border-bottom-color:var(--bs-border-color)}.direct-chat .card-body{position:relative;padding:0;overflow-x:hidden}.direct-chat.chat-pane-open .direct-chat-contacts{transform:translate(0,0)}.direct-chat.timestamp-light .direct-chat-timestamp{color:rgba(var(--bs-body-color-rgb),.65)}.direct-chat.timestamp-dark .direct-chat-timestamp{color:rgba(var(--bs-body-color-rgb),.9)}.direct-chat-messages{height:250px;padding:10px;overflow:auto;transform:translate(0,0)}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg::after{display:block;clear:both;content:""}.direct-chat-msg{margin-bottom:10px}.direct-chat-contacts,.direct-chat-messages{transition:transform .5s ease-in-out}@media (prefers-reduced-motion:reduce){.direct-chat-contacts,.direct-chat-messages{transition:none}}.direct-chat-text{border-radius:.5rem;position:relative;padding:5px 10px;margin:5px 0 0 50px;color:var(--bs-emphasis-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color)}.direct-chat-text::after,.direct-chat-text::before{position:absolute;top:15px;right:100%;width:0;height:0;pointer-events:none;content:" ";border:solid transparent;border-right-color:var(--bs-border-color)}.direct-chat-text::after{margin-top:-5px;border-width:5px}.direct-chat-text::before{margin-top:-6px;border-width:6px}.end .direct-chat-text{margin-right:50px;margin-left:0}.end .direct-chat-text::after,.end .direct-chat-text::before{right:auto;left:100%;border-right-color:transparent;border-left-color:var(--bs-border-color)}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.end .direct-chat-img{float:right}.direct-chat-infos{display:block;margin-bottom:2px;font-size:.875rem}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:rgba(var(--bs-body-color-rgb),.75)}.direct-chat-contacts-open .direct-chat-contacts{transform:translate(0,0)}.direct-chat-contacts{position:absolute;top:0;bottom:0;width:100%;height:250px;overflow:auto;color:var(--bs-body-bg);background-color:var(--bs-body-color);transform:translate(101%,0)}.direct-chat-contacts-light{background-color:var(--bs-light-bg-subtle)}.direct-chat-contacts-light .contacts-list-name{color:var(--bs-body-color)}.direct-chat-contacts-light .contacts-list-date{color:var(--bs-secondary-color)}.direct-chat-contacts-light .contacts-list-msg{color:var(--bs-secondary-color)}.contacts-list{padding-left:0;list-style:none}.contacts-list>li::after{display:block;clear:both;content:""}.contacts-list>li{padding:10px;margin:0;text-decoration:none;border-bottom:1px solid rgba(0,0,0,.2)}.contacts-list>li:last-of-type{border-bottom:0}.contacts-list>li a{text-decoration:none}.contacts-list-img{border-radius:50%;float:left;width:40px}.contacts-list-info{margin-left:45px;color:var(--bs-body-bg)}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:.875rem}.contacts-list-date{font-weight:400;color:var(--bs-secondary-bg)}.contacts-list-msg{color:var(--bs-secondary-bg)}.end>.direct-chat-text{color:var(--lte-direct-chat-color);background-color:var(--lte-direct-chat-bg);border-color:var(--lte-direct-chat-bg)}.end>.direct-chat-text::after,.end>.direct-chat-text::before{border-left-color:var(--lte-direct-chat-bg)}.direct-chat-primary{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#0d6efd}.direct-chat-secondary{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#6c757d}.direct-chat-success{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#198754}.direct-chat-info{--lte-direct-chat-color:#000;--lte-direct-chat-bg:#0dcaf0}.direct-chat-warning{--lte-direct-chat-color:#000;--lte-direct-chat-bg:#ffc107}.direct-chat-danger{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#dc3545}.direct-chat-light{--lte-direct-chat-color:#000;--lte-direct-chat-bg:#f8f9fa}.direct-chat-dark{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#212529}.toast-primary{--bs-toast-header-color:#fff;--bs-toast-header-bg:#0d6efd;--bs-toast-header-border-color:#0d6efd;--bs-toast-border-color:#0d6efd;--bs-toast-bg:var(--bs-primary-bg-subtle)}.toast-primary .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-secondary{--bs-toast-header-color:#fff;--bs-toast-header-bg:#6c757d;--bs-toast-header-border-color:#6c757d;--bs-toast-border-color:#6c757d;--bs-toast-bg:var(--bs-secondary-bg-subtle)}.toast-secondary .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-success{--bs-toast-header-color:#fff;--bs-toast-header-bg:#198754;--bs-toast-header-border-color:#198754;--bs-toast-border-color:#198754;--bs-toast-bg:var(--bs-success-bg-subtle)}.toast-success .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-info{--bs-toast-header-color:#000;--bs-toast-header-bg:#0dcaf0;--bs-toast-header-border-color:#0dcaf0;--bs-toast-border-color:#0dcaf0;--bs-toast-bg:var(--bs-info-bg-subtle)}.toast-warning{--bs-toast-header-color:#000;--bs-toast-header-bg:#ffc107;--bs-toast-header-border-color:#ffc107;--bs-toast-border-color:#ffc107;--bs-toast-bg:var(--bs-warning-bg-subtle)}.toast-danger{--bs-toast-header-color:#fff;--bs-toast-header-bg:#dc3545;--bs-toast-header-border-color:#dc3545;--bs-toast-border-color:#dc3545;--bs-toast-bg:var(--bs-danger-bg-subtle)}.toast-danger .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-light{--bs-toast-header-color:#000;--bs-toast-header-bg:#f8f9fa;--bs-toast-header-border-color:#f8f9fa;--bs-toast-border-color:#f8f9fa;--bs-toast-bg:var(--bs-light-bg-subtle)}.toast-dark{--bs-toast-header-color:#fff;--bs-toast-header-bg:#212529;--bs-toast-header-border-color:#212529;--bs-toast-border-color:#212529;--bs-toast-bg:var(--bs-dark-bg-subtle)}.toast-dark .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}[data-bs-theme=dark] .toast-info .btn-close{--bs-btn-close-white-filter:none}[data-bs-theme=dark] .toast-warning .btn-close{--bs-btn-close-white-filter:none}[data-bs-theme=dark] .toast-light .btn-close{--bs-btn-close-white-filter:none}.login-logo,.register-logo{margin-bottom:.9rem;font-size:2.1rem;font-weight:300;text-align:center}.login-logo a,.register-logo a{color:var(--bs-secondary-color);text-decoration:none}.login-page,.register-page{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}.login-box,.register-box{width:360px}@media (max-width:576px){.login-box,.register-box{width:90%;margin-top:.5rem}}.login-box .card,.register-box .card{margin-bottom:0}.login-card-body,.register-card-body{padding:20px;color:var(--bs-secondary-color);background-color:var(--bs-body-bg);border-top:0}.login-card-body .input-group .form-control:focus,.register-card-body .input-group .form-control:focus{box-shadow:none}.login-card-body .input-group .form-control:focus~.input-group-append .input-group-text,.login-card-body .input-group .form-control:focus~.input-group-prepend .input-group-text,.register-card-body .input-group .form-control:focus~.input-group-append .input-group-text,.register-card-body .input-group .form-control:focus~.input-group-prepend .input-group-text{border-color:rgb(134,182.5,254)}.login-card-body .input-group .form-control.is-valid:focus,.register-card-body .input-group .form-control.is-valid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text,.login-card-body .input-group .form-control.is-valid~.input-group-prepend .input-group-text,.register-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-valid~.input-group-prepend .input-group-text{border-color:#198754}.login-card-body .input-group .form-control.is-invalid:focus,.register-card-body .input-group .form-control.is-invalid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text{border-color:#dc3545}.login-card-body .input-group .input-group-text,.register-card-body .input-group .input-group-text{color:var(--bs-secondary-color);background-color:transparent;border-top-right-radius:.375rem;border-bottom-right-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.login-card-body .input-group .input-group-text,.register-card-body .input-group .input-group-text{transition:none}}.login-box-msg,.register-box-msg{padding:0 20px 20px;margin:0;text-align:center}.social-auth-links{margin:10px 0}.lockscreen .lockscreen-name{font-weight:600;text-align:center}.lockscreen .lockscreen-logo{margin-bottom:25px;font-size:35px;font-weight:300;text-align:center}.lockscreen .lockscreen-logo a{color:var(--bs-emphasis-color);text-decoration:none}.lockscreen .lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-item{position:relative;width:290px;padding:0;margin:10px auto 30px;background-color:var(--bs-body-bg);border-radius:4px}.lockscreen .lockscreen-image{position:absolute;top:-25px;left:-10px;z-index:10;padding:5px;background-color:var(--bs-body-bg);border-radius:50%}.lockscreen .lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen .lockscreen-credentials{margin-left:70px}.lockscreen .lockscreen-credentials .form-control{border:0}.lockscreen .lockscreen-credentials .btn{padding:0 10px;border:0}.lockscreen .lockscreen-footer{margin-top:10px}.img-size-32,.img-size-50,.img-size-64{height:auto}.img-size-64{width:64px}.img-size-50{width:50px}.img-size-32{width:32px}.skip-link{position:absolute;top:-40px;left:6px;z-index:999999;padding:8px 16px;font-weight:600;color:var(--bs-white);text-decoration:none;background:var(--bs-primary)}.skip-link:focus{top:0;outline:3px solid var(--bs-warning);outline-offset:2px}.skip-link:hover{color:var(--bs-white);text-decoration:none;background:var(--bs-primary-emphasis)}.focus-enhanced:focus{outline:3px solid var(--bs-focus-ring-color,#0d6efd);outline-offset:2px;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}@media (prefers-contrast:high){.card{border:2px solid}.btn{border-width:2px}.nav-link{border:1px solid transparent}.nav-link:focus,.nav-link:hover{border-color:currentcolor}}@media (prefers-reduced-motion:reduce){*,::after,::before{transition-duration:0s!important;animation-duration:0s!important;animation-iteration-count:1!important;scroll-behavior:auto!important}.fade{opacity:1!important;transition:none!important}.collapse{transition:none!important}.modal.fade .modal-dialog{transform:none!important}}.sr-only{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}.sr-only-focusable:focus{position:static!important;width:auto!important;height:auto!important;padding:inherit!important;margin:inherit!important;overflow:visible!important;clip:auto!important;white-space:normal!important}.focus-trap:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.text-accessible-primary{color:#003d82}.text-accessible-success{color:#0f5132}.text-accessible-danger{color:#842029}.text-accessible-warning{color:#664d03}.live-region{position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden}.live-region.live-region-visible{position:static;left:auto;width:auto;height:auto;overflow:visible}.form-control.is-invalid{border-color:var(--bs-danger)}.form-control.is-invalid:focus{border-color:var(--bs-danger);box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.invalid-feedback{display:block;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-danger)}.invalid-feedback[role=alert]{font-weight:600}.touch-target{min-width:44px;min-height:44px}.touch-target.touch-target-small{min-width:24px;min-height:24px}.table-accessible th{font-weight:600;background-color:var(--bs-secondary-bg)}.table-accessible th[scope=col]{border-bottom:2px solid var(--bs-border-color)}.table-accessible th[scope=row]{border-right:2px solid var(--bs-border-color)}.table-accessible caption{padding:.75rem;font-weight:600;color:var(--bs-secondary);text-align:left;caption-side:top}nav[role=navigation]:not([aria-label]):not([aria-labelledby])::before{position:absolute;left:-10000px;content:"Navigation"}fieldset{padding:1rem;margin-bottom:1rem;border:1px solid var(--bs-border-color)}fieldset legend{padding:0 .5rem;margin-bottom:.5rem;font-size:1.1em;font-weight:600}.loading[aria-busy=true]{position:relative;pointer-events:none}.loading[aria-busy=true]::after{position:absolute;top:50%;left:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;content:"";border:2px solid var(--bs-primary);border-top-color:transparent;animation:spin 1s linear infinite}@media (prefers-reduced-motion:reduce){.loading[aria-busy=true]::after{border-top-color:var(--bs-primary);animation:none}}@keyframes spin{to{transform:rotate(360deg)}}[data-bs-theme=dark] .text-accessible-primary{color:#6ea8fe}[data-bs-theme=dark] .text-accessible-success{color:#75b798}[data-bs-theme=dark] .text-accessible-danger{color:#f1aeb5}[data-bs-theme=dark] .text-accessible-warning{color:#ffda6a}@media print{.btn,.nav-link,.skip-link{color:#000!important;background:0 0!important;border:1px solid #000!important}a[href^=http]::after{font-size:.8em;content:" (" attr(href) ")"}} +@charset "UTF-8";/*! + * AdminLTE v4.0.0-rc5 + * Author: Colorlib + * Website: AdminLTE.io + * License: Open source - MIT + */:root,[data-bs-theme=light]{--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-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--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-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:rgb(5.2, 44, 101.2);--bs-secondary-text-emphasis:rgb(43.2, 46.8, 50);--bs-success-text-emphasis:rgb(10, 54, 33.6);--bs-info-text-emphasis:rgb(5.2, 80.8, 96);--bs-warning-text-emphasis:rgb(102, 77.2, 2.8);--bs-danger-text-emphasis:rgb(88, 21.2, 27.6);--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:rgb(206.6, 226, 254.6);--bs-secondary-bg-subtle:rgb(225.6, 227.4, 229);--bs-success-bg-subtle:rgb(209, 231, 220.8);--bs-info-bg-subtle:rgb(206.6, 244.4, 252);--bs-warning-bg-subtle:rgb(255, 242.6, 205.4);--bs-danger-bg-subtle:rgb(248, 214.6, 217.8);--bs-light-bg-subtle:rgb(251.5, 252, 252.5);--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:rgb(158.2, 197, 254.2);--bs-secondary-border-subtle:rgb(196.2, 199.8, 203);--bs-success-border-subtle:rgb(163, 207, 186.6);--bs-info-border-subtle:rgb(158.2, 233.8, 249);--bs-warning-border-subtle:rgb(255, 230.2, 155.8);--bs-danger-border-subtle:rgb(241, 174.2, 180.6);--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:"Source Sans 3",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,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));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:rgb(10.4, 88, 202.4);--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:rgb(255, 242.6, 205.4);--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:rgb(42.5, 47.5, 52.5);--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:rgb(109.8, 168, 253.8);--bs-secondary-text-emphasis:rgb(166.8, 172.2, 177);--bs-success-text-emphasis:rgb(117, 183, 152.4);--bs-info-text-emphasis:rgb(109.8, 223.2, 246);--bs-warning-text-emphasis:rgb(255, 217.8, 106.2);--bs-danger-text-emphasis:rgb(234, 133.8, 143.4);--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:rgb(2.6, 22, 50.6);--bs-secondary-bg-subtle:rgb(21.6, 23.4, 25);--bs-success-bg-subtle:rgb(5, 27, 16.8);--bs-info-bg-subtle:rgb(2.6, 40.4, 48);--bs-warning-bg-subtle:rgb(51, 38.6, 1.4);--bs-danger-bg-subtle:rgb(44, 10.6, 13.8);--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:rgb(7.8, 66, 151.8);--bs-secondary-border-subtle:rgb(64.8, 70.2, 75);--bs-success-border-subtle:rgb(15, 81, 50.4);--bs-info-border-subtle:rgb(7.8, 121.2, 144);--bs-warning-border-subtle:rgb(153, 115.8, 4.2);--bs-danger-border-subtle:rgb(132, 31.8, 41.4);--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:rgb(109.8, 168, 253.8);--bs-link-hover-color:rgb(138.84, 185.4, 254.04);--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:rgb(230.4, 132.6, 181.2);--bs-highlight-color:#dee2e6;--bs-highlight-bg:rgb(102, 77.2, 2.8);--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:rgb(117, 183, 152.4);--bs-form-valid-border-color:rgb(117, 183, 152.4);--bs-form-invalid-color:rgb(234, 133.8, 143.4);--bs-form-invalid-border-color:rgb(234, 133.8, 143.4)}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.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;color:var(--bs-heading-color)}.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[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:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}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}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:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}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:var(--bs-secondary-color);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]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[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;line-height:inherit;font-size:calc(1.275rem + .3vw)}@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]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-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-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@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:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-sm);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);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}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1600px;--bs-breakpoint-xxl:2000px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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{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.33333333%}.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.66666667%}.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-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:rgb(206.6, 226, 254.6);--bs-table-border-color:rgb(165.28, 180.8, 203.68);--bs-table-striped-bg:rgb(196.27, 214.7, 241.87);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(185.94, 203.4, 229.14);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(191.105, 209.05, 235.505);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:rgb(225.6, 227.4, 229);--bs-table-border-color:rgb(180.48, 181.92, 183.2);--bs-table-striped-bg:rgb(214.32, 216.03, 217.55);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(203.04, 204.66, 206.1);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(208.68, 210.345, 211.825);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:rgb(209, 231, 220.8);--bs-table-border-color:rgb(167.2, 184.8, 176.64);--bs-table-striped-bg:rgb(198.55, 219.45, 209.76);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(188.1, 207.9, 198.72);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(193.325, 213.675, 204.24);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:rgb(206.6, 244.4, 252);--bs-table-border-color:rgb(165.28, 195.52, 201.6);--bs-table-striped-bg:rgb(196.27, 232.18, 239.4);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(185.94, 219.96, 226.8);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(191.105, 226.07, 233.1);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:rgb(255, 242.6, 205.4);--bs-table-border-color:rgb(204, 194.08, 164.32);--bs-table-striped-bg:rgb(242.25, 230.47, 195.13);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(229.5, 218.34, 184.86);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(235.875, 224.405, 189.995);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:rgb(248, 214.6, 217.8);--bs-table-border-color:rgb(198.4, 171.68, 174.24);--bs-table-striped-bg:rgb(235.6, 203.87, 206.91);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(223.2, 193.14, 196.02);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(229.4, 198.505, 201.465);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:rgb(198.4, 199.2, 200);--bs-table-striped-bg:rgb(235.6, 236.55, 237.5);--bs-table-striped-color:#000;--bs-table-active-bg:rgb(223.2, 224.1, 225);--bs-table-active-color:#000;--bs-table-hover-bg:rgb(229.4, 230.325, 231.25);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:rgb(77.4, 80.6, 83.8);--bs-table-striped-bg:rgb(44.1, 47.9, 51.7);--bs-table-striped-color:#fff;--bs-table-active-bg:rgb(55.2, 58.8, 62.4);--bs-table-active-color:#fff;--bs-table-hover-bg:rgb(49.65, 53.35, 57.05);--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.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 + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-inset);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:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:rgb(134,182.5,254);outline:0;box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);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:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline: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 + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img: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 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-inset);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:rgb(134,182.5,254);outline:0;box-shadow:var(--bs-box-shadow-inset),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:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.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-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;print-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:rgb(134,182.5,254);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]{--bs-form-check-bg-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 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-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;--bs-form-check-bg-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{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: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");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);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{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgb%28134, 182.5, 254%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: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-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.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}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.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;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-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}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:rgb(182.4,211.5,254.4)}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem;box-shadow:var(--bs-box-shadow-inset)}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-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}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:rgb(182.4,211.5,254.4)}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem;box-shadow:var(--bs-box-shadow-inset)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem .75rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) 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,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder),.form-floating>.form-control:not(:-moz-placeholder){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:.75rem}.form-floating>.form-control:not(:-moz-placeholder)~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:not(:-moz-placeholder)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:var(--bs-secondary-bg)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.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:var(--bs-border-radius-lg)}.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:var(--bs-border-radius-sm)}.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)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){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>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){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:calc(-1 * var(--bs-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.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:var(--bs-success);border-radius:var(--bs-border-radius)}.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:var(--bs-form-valid-border-color);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.73.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.1'/%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:var(--bs-form-valid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-success-rgb),.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:var(--bs-form-valid-border-color)}.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"]{--bs-form-select-bg-icon: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.73.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.1'/%3e%3c/svg%3e");padding-right:4.125rem;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:var(--bs-form-valid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.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:var(--bs-danger);border-radius:var(--bs-border-radius)}.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:var(--bs-form-invalid-border-color);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:var(--bs-form-invalid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-danger-rgb),.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:var(--bs-form-invalid-border-color)}.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"]{--bs-form-select-bg-icon: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");padding-right:4.125rem;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:var(--bs-form-invalid-border-color);box-shadow:var(--bs-box-shadow-inset),0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);box-shadow:var(--bs-btn-box-shadow);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:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-box-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-box-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color);box-shadow:var(--bs-btn-active-shadow)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-active-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-active-shadow),var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity);box-shadow:none}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(11.05, 93.5, 215.05);--bs-btn-hover-border-color:rgb(10.4, 88, 202.4);--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(10.4, 88, 202.4);--bs-btn-active-border-color:rgb(9.75, 82.5, 189.75);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(91.8, 99.45, 106.25);--bs-btn-hover-border-color:rgb(86.4, 93.6, 100);--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(86.4, 93.6, 100);--bs-btn-active-border-color:rgb(81, 87.75, 93.75);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(21.25, 114.75, 71.4);--bs-btn-hover-border-color:rgb(20, 108, 67.2);--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(20, 108, 67.2);--bs-btn-active-border-color:rgb(18.75, 101.25, 63);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgb(49.3, 209.95, 242.25);--bs-btn-hover-border-color:rgb(37.2, 207.3, 241.5);--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:rgb(61.4, 212.6, 243);--bs-btn-active-border-color:rgb(37.2, 207.3, 241.5);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgb(255, 202.3, 44.2);--bs-btn-hover-border-color:rgb(255, 199.2, 31.8);--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:rgb(255, 205.4, 56.6);--bs-btn-active-border-color:rgb(255, 199.2, 31.8);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(187, 45.05, 58.65);--bs-btn-hover-border-color:rgb(176, 42.4, 55.2);--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(176, 42.4, 55.2);--bs-btn-active-border-color:rgb(165, 39.75, 51.75);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgb(210.8, 211.65, 212.5);--bs-btn-hover-border-color:rgb(198.4, 199.2, 200);--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:rgb(198.4, 199.2, 200);--bs-btn-active-border-color:rgb(186, 186.75, 187.5);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:rgb(66.3, 69.7, 73.1);--bs-btn-hover-border-color:rgb(55.2, 58.8, 62.4);--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:rgb(77.4, 80.6, 83.8);--bs-btn-active-border-color:rgb(55.2, 58.8, 62.4);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.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}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{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{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius);box-shadow:var(--bs-dropdown-box-shadow)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-spacer)}.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:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-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{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.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.show .dropdown-toggle{box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.show .dropdown-toggle.btn-link{box-shadow:none}.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:calc(-1 * var(--bs-border-width))}.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:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;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:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;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{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:1rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.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:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@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 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.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-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.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:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg: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")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: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")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius);box-shadow:var(--bs-card-box-shadow)}.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:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@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{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='rgb%285.2, 44, 101.2%29' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.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-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgb%28109.8, 168, 253.8%29'%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-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgb%28109.8, 168, 253.8%29'%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-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);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:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item .page-link{border-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius);box-shadow:var(--bs-progress-box-shadow)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@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:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.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:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);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:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg: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 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@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% - var(--bs-modal-margin) * 2)}.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% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);box-shadow:var(--bs-modal-box-shadow);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-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-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@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-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@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-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);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;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);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;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius);box-shadow:var(--bs-popover-box-shadow)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.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:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.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%;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;filter:var(--bs-carousel-control-icon-filter);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 0'/%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-.708'/%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%}.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:var(--bs-carousel-indicator-active-bg);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:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;flex-shrink:0;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.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:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.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:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){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}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!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}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.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}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y: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-inline-grid{display:inline-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:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{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:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!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}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.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}.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}.m-n1{margin:-.25rem!important}.m-n2{margin:-.5rem!important}.m-n3{margin:-1rem!important}.m-n4{margin:-1.5rem!important}.m-n5{margin:-3rem!important}.mx-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-n1{margin-top:-.25rem!important}.mt-n2{margin-top:-.5rem!important}.mt-n3{margin-top:-1rem!important}.mt-n4{margin-top:-1.5rem!important}.mt-n5{margin-top:-3rem!important}.me-n1{margin-right:-.25rem!important}.me-n2{margin-right:-.5rem!important}.me-n3{margin-right:-1rem!important}.me-n4{margin-right:-1.5rem!important}.me-n5{margin-right:-3rem!important}.mb-n1{margin-bottom:-.25rem!important}.mb-n2{margin-bottom:-.5rem!important}.mb-n3{margin-bottom:-1rem!important}.mb-n4{margin-bottom:-1.5rem!important}.mb-n5{margin-bottom:-3rem!important}.ms-n1{margin-left:-.25rem!important}.ms-n2{margin-left:-.5rem!important}.ms-n3{margin-left:-1rem!important}.ms-n4{margin-left:-1.5rem!important}.ms-n5{margin-left:-3rem!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}.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}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap: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}.fs-7{font-size:.875rem!important}.fs-8{font-size:.75rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!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{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!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:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-sm-n1{margin:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.m-sm-n3{margin:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mx-sm-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-sm-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-sm-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-sm-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-sm-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-sm-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-sm-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-sm-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-sm-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-sm-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-sm-n1{margin-top:-.25rem!important}.mt-sm-n2{margin-top:-.5rem!important}.mt-sm-n3{margin-top:-1rem!important}.mt-sm-n4{margin-top:-1.5rem!important}.mt-sm-n5{margin-top:-3rem!important}.me-sm-n1{margin-right:-.25rem!important}.me-sm-n2{margin-right:-.5rem!important}.me-sm-n3{margin-right:-1rem!important}.me-sm-n4{margin-right:-1.5rem!important}.me-sm-n5{margin-right:-3rem!important}.mb-sm-n1{margin-bottom:-.25rem!important}.mb-sm-n2{margin-bottom:-.5rem!important}.mb-sm-n3{margin-bottom:-1rem!important}.mb-sm-n4{margin-bottom:-1.5rem!important}.mb-sm-n5{margin-bottom:-3rem!important}.ms-sm-n1{margin-left:-.25rem!important}.ms-sm-n2{margin-left:-.5rem!important}.ms-sm-n3{margin-left:-1rem!important}.ms-sm-n4{margin-left:-1.5rem!important}.ms-sm-n5{margin-left:-3rem!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}.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}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-md-n1{margin:-.25rem!important}.m-md-n2{margin:-.5rem!important}.m-md-n3{margin:-1rem!important}.m-md-n4{margin:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mx-md-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-md-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-md-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-md-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-md-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-md-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-md-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-md-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-md-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-md-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-md-n1{margin-top:-.25rem!important}.mt-md-n2{margin-top:-.5rem!important}.mt-md-n3{margin-top:-1rem!important}.mt-md-n4{margin-top:-1.5rem!important}.mt-md-n5{margin-top:-3rem!important}.me-md-n1{margin-right:-.25rem!important}.me-md-n2{margin-right:-.5rem!important}.me-md-n3{margin-right:-1rem!important}.me-md-n4{margin-right:-1.5rem!important}.me-md-n5{margin-right:-3rem!important}.mb-md-n1{margin-bottom:-.25rem!important}.mb-md-n2{margin-bottom:-.5rem!important}.mb-md-n3{margin-bottom:-1rem!important}.mb-md-n4{margin-bottom:-1.5rem!important}.mb-md-n5{margin-bottom:-3rem!important}.ms-md-n1{margin-left:-.25rem!important}.ms-md-n2{margin-left:-.5rem!important}.ms-md-n3{margin-left:-1rem!important}.ms-md-n4{margin-left:-1.5rem!important}.ms-md-n5{margin-left:-3rem!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}.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}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-lg-n1{margin:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.m-lg-n3{margin:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mx-lg-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-lg-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-lg-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-lg-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-lg-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-lg-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-lg-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-lg-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-lg-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-lg-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-lg-n1{margin-top:-.25rem!important}.mt-lg-n2{margin-top:-.5rem!important}.mt-lg-n3{margin-top:-1rem!important}.mt-lg-n4{margin-top:-1.5rem!important}.mt-lg-n5{margin-top:-3rem!important}.me-lg-n1{margin-right:-.25rem!important}.me-lg-n2{margin-right:-.5rem!important}.me-lg-n3{margin-right:-1rem!important}.me-lg-n4{margin-right:-1.5rem!important}.me-lg-n5{margin-right:-3rem!important}.mb-lg-n1{margin-bottom:-.25rem!important}.mb-lg-n2{margin-bottom:-.5rem!important}.mb-lg-n3{margin-bottom:-1rem!important}.mb-lg-n4{margin-bottom:-1.5rem!important}.mb-lg-n5{margin-bottom:-3rem!important}.ms-lg-n1{margin-left:-.25rem!important}.ms-lg-n2{margin-left:-.5rem!important}.ms-lg-n3{margin-left:-1rem!important}.ms-lg-n4{margin-left:-1.5rem!important}.ms-lg-n5{margin-left:-3rem!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}.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}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-xl-n1{margin:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.m-xl-n3{margin:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mx-xl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xl-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xl-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xl-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-xl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xl-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xl-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xl-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-xl-n1{margin-top:-.25rem!important}.mt-xl-n2{margin-top:-.5rem!important}.mt-xl-n3{margin-top:-1rem!important}.mt-xl-n4{margin-top:-1.5rem!important}.mt-xl-n5{margin-top:-3rem!important}.me-xl-n1{margin-right:-.25rem!important}.me-xl-n2{margin-right:-.5rem!important}.me-xl-n3{margin-right:-1rem!important}.me-xl-n4{margin-right:-1.5rem!important}.me-xl-n5{margin-right:-3rem!important}.mb-xl-n1{margin-bottom:-.25rem!important}.mb-xl-n2{margin-bottom:-.5rem!important}.mb-xl-n3{margin-bottom:-1rem!important}.mb-xl-n4{margin-bottom:-1.5rem!important}.mb-xl-n5{margin-bottom:-3rem!important}.ms-xl-n1{margin-left:-.25rem!important}.ms-xl-n2{margin-left:-.5rem!important}.ms-xl-n3{margin-left:-1rem!important}.ms-xl-n4{margin-left:-1.5rem!important}.ms-xl-n5{margin-left:-3rem!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}.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}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap: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}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit: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-inline-grid{display:inline-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}.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}.m-xxl-n1{margin:-.25rem!important}.m-xxl-n2{margin:-.5rem!important}.m-xxl-n3{margin:-1rem!important}.m-xxl-n4{margin:-1.5rem!important}.m-xxl-n5{margin:-3rem!important}.mx-xxl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xxl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xxl-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xxl-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xxl-n5{margin-right:-3rem!important;margin-left:-3rem!important}.my-xxl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xxl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xxl-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xxl-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xxl-n5{margin-top:-3rem!important;margin-bottom:-3rem!important}.mt-xxl-n1{margin-top:-.25rem!important}.mt-xxl-n2{margin-top:-.5rem!important}.mt-xxl-n3{margin-top:-1rem!important}.mt-xxl-n4{margin-top:-1.5rem!important}.mt-xxl-n5{margin-top:-3rem!important}.me-xxl-n1{margin-right:-.25rem!important}.me-xxl-n2{margin-right:-.5rem!important}.me-xxl-n3{margin-right:-1rem!important}.me-xxl-n4{margin-right:-1.5rem!important}.me-xxl-n5{margin-right:-3rem!important}.mb-xxl-n1{margin-bottom:-.25rem!important}.mb-xxl-n2{margin-bottom:-.5rem!important}.mb-xxl-n3{margin-bottom:-1rem!important}.mb-xxl-n4{margin-bottom:-1.5rem!important}.mb-xxl-n5{margin-bottom:-3rem!important}.ms-xxl-n1{margin-left:-.25rem!important}.ms-xxl-n2{margin-left:-.5rem!important}.ms-xxl-n3{margin-left:-1rem!important}.ms-xxl-n4{margin-left:-1.5rem!important}.ms-xxl-n5{margin-left:-3rem!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}.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}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap: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-inline-grid{display:inline-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}}@keyframes flipInX{0%{opacity:0;transition-timing-function:ease-in;transform:perspective(400px) rotate3d(1,0,0,90deg)}40%{transition-timing-function:ease-in;transform:perspective(400px) rotate3d(1,0,0,-20deg)}60%{opacity:1;transform:perspective(400px) rotate3d(1,0,0,10deg)}80%{transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{transform:perspective(400px)}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}@keyframes shake{0%{transform:translate(2px,1px) rotate(0)}10%{transform:translate(-1px,-2px) rotate(-2deg)}20%{transform:translate(-3px,0) rotate(3deg)}30%{transform:translate(0,2px) rotate(0)}40%{transform:translate(1px,-1px) rotate(1deg)}50%{transform:translate(-1px,2px) rotate(-1deg)}60%{transform:translate(-3px,1px) rotate(0)}70%{transform:translate(2px,1px) rotate(-2deg)}80%{transform:translate(-1px,-1px) rotate(4deg)}90%{transform:translate(2px,2px) rotate(0)}100%{transform:translate(1px,-2px) rotate(-1deg)}}@keyframes wobble{0%{transform:none}15%{transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{transform:none}}:root,[data-bs-theme=light]{--lte-sidebar-width:250px}.app-wrapper{position:relative;display:grid;grid-template-areas:"lte-app-sidebar lte-app-header" "lte-app-sidebar lte-app-main" "lte-app-sidebar lte-app-footer";grid-template-rows:min-content 1fr min-content;grid-template-columns:auto 1fr;grid-gap:0;align-content:stretch;align-items:stretch;max-width:100vw;min-height:100vh}.app-wrapper>*{min-width:0}.app-content{padding:0 .5rem}.app-header{z-index:1034;grid-area:lte-app-header;max-width:100vw;border-bottom:1px solid var(--bs-border-color);transition:.3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-header{transition:none}}.app-header .nav-link{position:relative;height:2.5rem}.navbar-badge{position:absolute;top:9px;right:5px;padding:2px 4px;font-size:.6rem;font-weight:400}.fixed-header .app-header{position:sticky;top:0;z-index:1030}.app-sidebar{--lte-sidebar-hover-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-color:#343a40;--lte-sidebar-hover-color:#212529;--lte-sidebar-active-color:#000;--lte-sidebar-menu-active-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-menu-active-color:#000;--lte-sidebar-submenu-bg:transparent;--lte-sidebar-submenu-color:#777;--lte-sidebar-submenu-hover-color:#000;--lte-sidebar-submenu-hover-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-submenu-active-color:#212529;--lte-sidebar-submenu-active-bg:rgba(0, 0, 0, 0.1);--lte-sidebar-header-color:rgb(49.4, 55.1, 60.8);z-index:1038;grid-area:lte-app-sidebar;min-width:var(--lte-sidebar-width);max-width:var(--lte-sidebar-width);transition:min-width .3s ease-in-out,max-width .3s ease-in-out,margin-left .3s ease-in-out,margin-right .3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-sidebar{transition:none}}.sidebar-brand{display:flex;align-items:center;justify-content:center;height:3.5rem;padding:.8125rem .5rem;overflow:hidden;font-size:1.25rem;white-space:nowrap;border-bottom:1px solid var(--bs-border-color);transition:width .3s ease-in-out}@media (prefers-reduced-motion:reduce){.sidebar-brand{transition:none}}.sidebar-brand .brand-link{display:flex;align-items:center;text-decoration:none}.sidebar-brand .brand-link .brand-image{float:left;width:auto;max-height:33px;line-height:.8}.sidebar-brand .brand-link .brand-image-xs{float:left;width:auto;max-height:33px;margin-top:-.1rem;line-height:.8}.sidebar-brand .brand-link .brand-image-xl{width:auto;max-height:40px;line-height:.8}.sidebar-brand .brand-link .brand-image-xl.single{margin-top:-.3rem}.sidebar-brand .brand-text{margin-left:.5rem;color:rgba(var(--bs-emphasis-color-rgb),.8);transition:flex .3s ease-in-out,width .3s ease-in-out}@media (prefers-reduced-motion:reduce){.sidebar-brand .brand-text{transition:none}}.sidebar-brand .brand-text:hover{color:var(--bs-emphasis-color)}.sidebar-wrapper{padding-top:.5rem;padding-right:.5rem;padding-bottom:.5rem;padding-left:.5rem;scrollbar-color:var(--bs-secondary-bg) transparent}.sidebar-wrapper::-webkit-scrollbar-thumb{background-color:var(--bs-secondary-bg)}.sidebar-wrapper::-webkit-scrollbar-track{background-color:transparent}.sidebar-wrapper::-webkit-scrollbar-corner{background-color:transparent}.sidebar-wrapper{scrollbar-width:thin}.sidebar-wrapper::-webkit-scrollbar{width:.5rem;height:.5rem}.sidebar-wrapper .nav-item{max-width:100%}.sidebar-wrapper .nav-link{display:flex;justify-content:flex-start}.sidebar-wrapper .nav-link p{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-wrapper .nav-icon{display:flex;align-items:center;justify-content:center;min-width:1.5rem;max-width:1.5rem}.sidebar-wrapper .sidebar-menu>.nav-item.menu-open .nav-link.active:not(:hover){--lte-sidebar-menu-active-bg:transparent}.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link:active,.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link:focus{color:var(--lte-sidebar-color)}.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link.active:not(:hover){color:var(--lte-sidebar-menu-active-color);background-color:var(--lte-sidebar-menu-active-bg)}.sidebar-wrapper .sidebar-menu>.nav-item.menu-open>.nav-link,.sidebar-wrapper .sidebar-menu>.nav-item:hover>.nav-link,.sidebar-wrapper .sidebar-menu>.nav-item>.nav-link:focus{color:var(--lte-sidebar-hover-color);background-color:var(--lte-sidebar-hover-bg)}.sidebar-wrapper .sidebar-menu>.nav-item>.nav-treeview{background-color:var(--lte-sidebar-submenu-bg)}.sidebar-wrapper .nav-header{color:var(--lte-sidebar-header-color);background-color:inherit}.sidebar-wrapper a{color:var(--lte-sidebar-color)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link{color:var(--lte-sidebar-submenu-color)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link:focus,.sidebar-wrapper .nav-treeview>.nav-item>.nav-link:hover{color:var(--lte-sidebar-submenu-hover-color)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link.active,.sidebar-wrapper .nav-treeview>.nav-item>.nav-link.active:focus,.sidebar-wrapper .nav-treeview>.nav-item>.nav-link.active:hover{color:var(--lte-sidebar-submenu-active-color);background-color:var(--lte-sidebar-submenu-active-bg)}.sidebar-wrapper .nav-treeview>.nav-item>.nav-link:hover{background-color:var(--lte-sidebar-submenu-hover-bg)}.sidebar-menu .nav-item>.nav-link{margin-bottom:.2rem}.sidebar-menu .nav-item>.nav-link .nav-arrow{transition:transform ease-in-out .3s}@media (prefers-reduced-motion:reduce){.sidebar-menu .nav-item>.nav-link .nav-arrow{transition:none}}.sidebar-menu .nav-item>.nav-link .nav-arrow{transform:translateY(-50%);animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-menu .nav-link>.nav-badge,.sidebar-menu .nav-link>p>.nav-badge{position:absolute;top:50%;right:1rem;transform:translateY(-50%)}.sidebar-menu .nav-link>.nav-arrow,.sidebar-menu .nav-link>p>.nav-arrow{position:absolute;top:50%;right:1rem}.sidebar-menu .nav-link{position:relative;width:100%;transition:width ease-in-out .3s}@media (prefers-reduced-motion:reduce){.sidebar-menu .nav-link{transition:none}}.sidebar-menu .nav-link{border-radius:.375rem}.sidebar-menu .nav-link p{display:inline;padding-left:.5rem;margin:0}.sidebar-menu .nav-header{padding:.5rem .75rem;font-size:.9rem}.sidebar-menu .nav-treeview{display:none;padding:0;list-style:none}.nav-indent .sidebar-menu .nav-treeview{padding-left:.5rem}.sidebar-menu .menu-open>.nav-treeview{display:block}.sidebar-menu .menu-open>.nav-link .nav-arrow{transform:translateY(-50%) rotate(90deg)}.sidebar-menu .nav-link>.nav-arrow,.sidebar-menu .nav-link>.nav-badge,.sidebar-menu .nav-link>p>.nav-arrow,.sidebar-menu .nav-link>p>.nav-badge{right:1rem!important;left:auto!important}.nav-compact.nav-indent .nav-treeview{padding-left:0}.nav-compact.nav-indent .nav-treeview .nav-item{padding-left:.5rem}.sidebar-mini.sidebar-collapse.nav-indent .app-sidebar:hover .nav-treeview{padding-left:0}.sidebar-mini.sidebar-collapse.nav-indent .app-sidebar:hover .nav-treeview .nav-item{padding-left:.5rem}.sidebar-collapse.nav-compact.nav-indent .nav-treeview .nav-item{padding-left:0}.nav-compact .nav-link{border-radius:0;margin-bottom:0!important}.sidebar-menu,.sidebar-menu .nav-link,.sidebar-menu>.nav-header{white-space:nowrap}.logo-xl,.logo-xs{position:absolute;visibility:visible;opacity:1}.logo-xl.brand-image-xs,.logo-xs.brand-image-xs{top:12px;left:18px}.logo-xl.brand-image-xl,.logo-xs.brand-image-xl{top:6px;left:12px}.logo-xs{visibility:hidden;opacity:0}.logo-xs.brand-image-xl{top:8px;left:16px}.brand-link.logo-switch::before{content:" "}.sidebar-mini.sidebar-collapse .app-sidebar{min-width:4.6rem;max-width:4.6rem}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-header{display:none}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link{width:3.6rem}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link p{display:inline-block;width:0;white-space:nowrap}.sidebar-mini.sidebar-collapse .sidebar-menu .nav-arrow,.sidebar-mini.sidebar-collapse .sidebar-menu .nav-badge{display:none;animation-name:fadeOut;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .brand-text{display:inline-block;max-width:0;overflow:hidden}.sidebar-mini.sidebar-collapse .brand-text,.sidebar-mini.sidebar-collapse .logo-xl,.sidebar-mini.sidebar-collapse .nav-arrow,.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link p{visibility:hidden;animation-name:fadeOut;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .logo-xs{display:inline-block;visibility:visible;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .app-sidebar:hover{min-width:var(--lte-sidebar-width);max-width:var(--lte-sidebar-width)}.sidebar-mini.sidebar-collapse .app-sidebar:hover .sidebar-menu .nav-header{display:inline-block}.sidebar-mini.sidebar-collapse .app-sidebar:hover .sidebar-menu .nav-link{width:auto}.sidebar-mini.sidebar-collapse .app-sidebar:hover .brand-text,.sidebar-mini.sidebar-collapse .app-sidebar:hover .logo-xl,.sidebar-mini.sidebar-collapse .app-sidebar:hover .sidebar-menu .nav-link p{width:auto;margin-left:0;visibility:visible;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .app-sidebar:hover .brand-text{display:inline;max-width:inherit;margin-left:.5rem;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both}.sidebar-mini.sidebar-collapse .app-sidebar:hover .nav-arrow,.sidebar-mini.sidebar-collapse .app-sidebar:hover .nav-badge{display:inline-block;visibility:visible;animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-mini.sidebar-collapse .app-sidebar:hover .nav-link p{padding-left:.5rem}.sidebar-mini.sidebar-collapse .app-sidebar:hover .logo-xs{visibility:hidden;animation-name:fadeOut;animation-duration:.3s;animation-fill-mode:both}.sidebar-collapse:not(.sidebar-mini) .app-sidebar{margin-left:calc(var(--lte-sidebar-width) * -1)}@media (min-width:576px){.sidebar-expand-sm.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-sm.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-sm.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-sm.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-sm.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-sm.sidebar-open .nav-link>.nav-badge,.sidebar-expand-sm.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-sm.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-sm.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:575.98px){.sidebar-expand-sm::before{display:none;content:"575.98px"}.sidebar-expand-sm .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-sm .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-sm.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-sm.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:768px){.sidebar-expand-md.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-md.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-md.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-md.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-md.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-md.sidebar-open .nav-link>.nav-badge,.sidebar-expand-md.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-md.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-md.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:767.98px){.sidebar-expand-md::before{display:none;content:"767.98px"}.sidebar-expand-md .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-md .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-md.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-md.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:992px){.sidebar-expand-lg.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-lg.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-lg.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-lg.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-lg.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-lg.sidebar-open .nav-link>.nav-badge,.sidebar-expand-lg.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-lg.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-lg.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:991.98px){.sidebar-expand-lg::before{display:none;content:"991.98px"}.sidebar-expand-lg .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-lg .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-lg.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-lg.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:1200px){.sidebar-expand-xl.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-xl.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-xl.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-xl.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-xl.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xl.sidebar-open .nav-link>.nav-badge,.sidebar-expand-xl.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-xl.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-xl.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:1199.98px){.sidebar-expand-xl::before{display:none;content:"1199.98px"}.sidebar-expand-xl .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-xl .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xl.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-xl.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}@media (min-width:1400px){.sidebar-expand-xxl.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand-xxl.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand-xxl.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand-xxl.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand-xxl.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xxl.sidebar-open .nav-link>.nav-badge,.sidebar-expand-xxl.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand-xxl.sidebar-open .nav-link>.nav-arrow,.sidebar-expand-xxl.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}}@media (max-width:1399.98px){.sidebar-expand-xxl::before{display:none;content:"1399.98px"}.sidebar-expand-xxl .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand-xxl .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand-xxl.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand-xxl.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}}.sidebar-expand.layout-fixed .app-main-wrapper{display:flex;flex-direction:column;min-height:100vh}.sidebar-expand.layout-fixed .app-sidebar-wrapper{position:relative}.sidebar-expand.layout-fixed .app-main{flex:1 1 auto;overflow:auto}.sidebar-expand.layout-fixed .app-sidebar{position:sticky;top:0;bottom:0;max-height:100vh}.sidebar-expand.layout-fixed .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand.sidebar-open .nav-link>.nav-badge,.sidebar-expand.sidebar-open .nav-link>p>.nav-badge{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand.sidebar-open .nav-link>.nav-arrow,.sidebar-expand.sidebar-open .nav-link>p>.nav-arrow{animation-name:fadeIn;animation-duration:.3s;animation-fill-mode:both;animation-delay:.3s}.sidebar-expand::before{display:none;content:""}.sidebar-expand .app-sidebar{position:fixed;top:0;bottom:0;max-height:100vh;margin-left:calc(var(--lte-sidebar-width) * -1)}.sidebar-expand .app-sidebar .sidebar-wrapper{height:calc(100vh - (calc(3.5rem + 1px)));overflow-x:hidden;overflow-y:auto}.sidebar-expand.sidebar-open .app-sidebar{margin-left:0}.sidebar-expand.sidebar-open .sidebar-overlay{position:absolute;inset:0;z-index:1037;width:100%;height:100%;cursor:pointer;visibility:visible;background-color:rgba(0,0,0,.2);animation-name:fadeIn;animation-fill-mode:both}.app-sidebar .brand-text,.app-sidebar .logo-xl,.app-sidebar .logo-xs,.sidebar-menu .nav-link p{transition:margin-left .3s linear,opacity .3s ease,visibility .3s ease}@media (prefers-reduced-motion:reduce){.app-sidebar .brand-text,.app-sidebar .logo-xl,.app-sidebar .logo-xs,.sidebar-menu .nav-link p{transition:none}}.app-loaded.sidebar-mini.sidebar-collapse .brand-text,.app-loaded.sidebar-mini.sidebar-collapse .sidebar-menu .nav-link p{animation-duration:.3s}body:not(.app-loaded) .app-footer,body:not(.app-loaded) .app-header,body:not(.app-loaded) .app-main,body:not(.app-loaded) .app-sidebar{transition:none!important}@media (prefers-reduced-motion:reduce){body:not(.app-loaded) .app-footer,body:not(.app-loaded) .app-header,body:not(.app-loaded) .app-main,body:not(.app-loaded) .app-sidebar{transition:none}}body:not(.app-loaded) .app-footer,body:not(.app-loaded) .app-header,body:not(.app-loaded) .app-main,body:not(.app-loaded) .app-sidebar{animation-duration:0s!important}.hold-transition .app-footer,.hold-transition .app-header,.hold-transition .app-main,.hold-transition .app-sidebar,.hold-transition .nav-arrow,.hold-transition .nav-badge{transition:none!important}@media (prefers-reduced-motion:reduce){.hold-transition .app-footer,.hold-transition .app-header,.hold-transition .app-main,.hold-transition .app-sidebar,.hold-transition .nav-arrow,.hold-transition .nav-badge{transition:none}}.hold-transition .app-footer,.hold-transition .app-header,.hold-transition .app-main,.hold-transition .app-sidebar,.hold-transition .nav-arrow,.hold-transition .nav-badge{animation-duration:0s!important}[data-bs-theme=dark] .app-sidebar,[data-bs-theme=dark].app-sidebar{--lte-sidebar-hover-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-color:#c2c7d0;--lte-sidebar-hover-color:#fff;--lte-sidebar-active-color:#fff;--lte-sidebar-menu-active-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-menu-active-color:#fff;--lte-sidebar-submenu-bg:transparent;--lte-sidebar-submenu-color:#c2c7d0;--lte-sidebar-submenu-hover-color:#fff;--lte-sidebar-submenu-hover-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-submenu-active-color:#fff;--lte-sidebar-submenu-active-bg:rgba(255, 255, 255, 0.1);--lte-sidebar-header-color:rgb(197.05, 201.8, 210.35)}.app-main{position:relative;display:flex;flex-direction:column;grid-area:lte-app-main;max-width:100vw;padding-bottom:.75rem;transition:.3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-main{transition:none}}.app-main .app-content-header{padding:1rem .5rem}.app-main .app-content-header .breadcrumb{padding:0;margin-bottom:0;line-height:2.5rem}.app-main .app-content-header .breadcrumb a{text-decoration:none}.app-main .app-content-bottom-area,.app-main .app-content-top-area{color:var(--bs-secondary-color);background-color:var(--bs-body-bg)}.app-main .app-content-top-area{padding:1rem 0;border-bottom:1px solid var(--bs-border-color)}.app-main .app-content-bottom-area{padding:1rem 0;margin-top:auto;margin-bottom:-.75rem;border-top:1px solid var(--bs-border-color)}.app-footer{grid-area:lte-app-footer;width:inherit;max-width:100vw;min-height:3rem;padding:1rem;color:var(--bs-secondary-color);background-color:var(--bs-body-bg);border-top:1px solid var(--bs-border-color);transition:.3s ease-in-out}@media (prefers-reduced-motion:reduce){.app-footer{transition:none}}.fixed-footer .app-footer{position:sticky;bottom:0;z-index:1030}.fs-7 .dropdown-menu{font-size:.875rem!important}.fs-7 .dropdown-toggle::after{vertical-align:.2rem}.dropdown-item-title{margin:0;font-size:1rem}.dropdown-icon::after{margin-left:0}.dropdown-menu-lg{min-width:280px;max-width:300px;padding:0}.dropdown-menu-lg .dropdown-divider{margin:0}.dropdown-menu-lg .dropdown-item{padding:.5rem 1rem}.dropdown-menu-lg p{margin:0;word-wrap:break-word;white-space:normal}.dropdown-submenu{position:relative}.dropdown-submenu>a::after{border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid;float:right;margin-top:.5rem;margin-left:.5rem}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:0;margin-left:0}.dropdown-hover .dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.nav-item.dropdown:hover>.dropdown-menu,.dropdown-hover:hover>.dropdown-menu{display:block}.dropdown-menu-xl{min-width:360px;max-width:420px;padding:0}.dropdown-menu-xl .dropdown-divider{margin:0}.dropdown-menu-xl .dropdown-item{padding:.5rem 1rem}.dropdown-menu-xl p{margin:0;word-wrap:break-word;white-space:normal}.dropdown-footer,.dropdown-header{display:block;padding:.5rem 1rem;font-size:.875rem;text-align:center}.open:not(.dropup)>.animated-dropdown-menu{animation:flipInX .7s both;backface-visibility:visible!important}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:575.98px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;background-color:var(--bs-body-bg);border:1px solid var(--bs-border-color)}}.navbar-nav>.user-menu>.nav-link::after{content:none}.navbar-nav>.user-menu>.dropdown-menu{border-top-left-radius:0;border-top-right-radius:0;width:280px;padding:0}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{min-height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;width:90px;height:90px;border:3px solid;border-color:transparent;border-color:var(--bs-border-color-translucent)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;margin-top:10px;font-size:17px;word-wrap:break-word}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>.small,.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body::after{display:block;clear:both;content:""}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-top:1px solid var(--bs-border-color);border-bottom:1px solid var(--bs-border-color-translucent)}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{text-decoration:none}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:var(--bs-body-color)!important;background-color:var(--bs-body-bg)!important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer::after{display:block;clear:both;content:""}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{padding:10px;background-color:var(--bs-light-bg)}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:var(--bs-body-color)}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:var(--bs-body-bg)}}@media (min-width:576px){.navbar-nav>.user-menu .user-image{float:none;margin-top:-8px;margin-right:.4rem;line-height:10px}}.navbar-nav>.user-menu .user-image{float:left;width:2rem;height:2rem;margin-top:-2px;border-radius:50%}.callout{--bs-link-color-rgb:var(--lte-callout-link);--bs-code-color:var(--lte-callout-code-color);padding:1.25rem;color:var(--lte-callout-color,inherit);background-color:var(--lte-callout-bg,var(--bs-gray-100));border-left:.25rem solid var(--lte-callout-border,var(--bs-gray-300))}.callout .callout-link{font-weight:700;color:var(--bs-callout-link-color)}.callout .h4,.callout h4{margin-bottom:.25rem}.callout>:last-child{margin-bottom:0}.callout+.callout{margin-top:-.25rem}.callout-primary{--lte-callout-color:var(--bs-primary-text-emphasis);--lte-callout-bg:var(--bs-primary-bg-subtle);--lte-callout-border:var(--bs-primary-border-subtle);--bs-callout-link-color:var(--bs-primary-text-emphasis)}.callout-secondary{--lte-callout-color:var(--bs-secondary-text-emphasis);--lte-callout-bg:var(--bs-secondary-bg-subtle);--lte-callout-border:var(--bs-secondary-border-subtle);--bs-callout-link-color:var(--bs-secondary-text-emphasis)}.callout-success{--lte-callout-color:var(--bs-success-text-emphasis);--lte-callout-bg:var(--bs-success-bg-subtle);--lte-callout-border:var(--bs-success-border-subtle);--bs-callout-link-color:var(--bs-success-text-emphasis)}.callout-info{--lte-callout-color:var(--bs-info-text-emphasis);--lte-callout-bg:var(--bs-info-bg-subtle);--lte-callout-border:var(--bs-info-border-subtle);--bs-callout-link-color:var(--bs-info-text-emphasis)}.callout-warning{--lte-callout-color:var(--bs-warning-text-emphasis);--lte-callout-bg:var(--bs-warning-bg-subtle);--lte-callout-border:var(--bs-warning-border-subtle);--bs-callout-link-color:var(--bs-warning-text-emphasis)}.callout-danger{--lte-callout-color:var(--bs-danger-text-emphasis);--lte-callout-bg:var(--bs-danger-bg-subtle);--lte-callout-border:var(--bs-danger-border-subtle);--bs-callout-link-color:var(--bs-danger-text-emphasis)}.callout-light{--lte-callout-color:var(--bs-light-text-emphasis);--lte-callout-bg:var(--bs-light-bg-subtle);--lte-callout-border:var(--bs-light-border-subtle);--bs-callout-link-color:var(--bs-light-text-emphasis)}.callout-dark{--lte-callout-color:var(--bs-dark-text-emphasis);--lte-callout-bg:var(--bs-dark-bg-subtle);--lte-callout-border:var(--bs-dark-border-subtle);--bs-callout-link-color:var(--bs-dark-text-emphasis)}.compact-mode .app-header{max-height:2.75rem}.compact-mode .app-header .nav-link{max-height:1.75rem}.compact-mode .nav-link{--bs-nav-link-padding-y:.25rem;--bs-nav-link-padding-x:.5rem}.compact-mode.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover){min-width:3.1rem;max-width:3.1rem}.compact-mode.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-menu .nav-link{width:2.1rem!important}.compact-mode .logo-xl,.compact-mode .logo-xs{max-height:2.75rem}.compact-mode .brand-image{width:1.75rem;height:1.75rem}.compact-mode .sidebar-brand{height:2.75rem}.compact-mode .app-footer{padding:.5rem}.compact-mode .sidebar-wrapper .nav-icon{min-width:1.1rem;max-width:1.1rem}.astro-code{padding:.75rem;border-radius:.375rem}.progress{border-radius:1px}.progress.vertical{position:relative;display:inline-block;width:30px;height:200px;margin-right:10px}.progress.vertical>.progress-bar{position:absolute;bottom:0;width:100%}.progress.vertical.progress-sm,.progress.vertical.sm{width:20px}.progress.vertical.progress-xs,.progress.vertical.xs{width:10px}.progress.vertical.progress-xxs,.progress.vertical.xxs{width:3px}.progress-group{margin-bottom:.5rem}.progress-sm{height:10px}.progress-xs{height:7px}.progress-xxs{height:3px}.table tr>td .progress{margin:0}.card{box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2)}.card[class*=card-]:not(.card-outline)>.card-header,.card[class*=text-bg-]:not(.card-outline)>.card-header{color:var(--lte-card-variant-color);background-color:var(--lte-card-variant-bg)}.card[class*=card-]:not(.card-outline)>.card-header .btn-tool,.card[class*=text-bg-]:not(.card-outline)>.card-header .btn-tool{--bs-btn-color:rgba(var(--lte-card-variant-color-rgb), .8);--bs-btn-hover-color:var(--lte-card-variant-color)}.card.card-outline{border-top:3px solid var(--lte-card-variant-bg)}.card.maximized-card{position:fixed;top:0;left:0;z-index:1050;width:100%!important;max-width:100%!important;height:100%!important;max-height:100%!important}.card.maximized-card.was-collapsed .card-body{display:block!important}.card.maximized-card .card-body{overflow:auto}.card.maximized-card [data-lte-toggle=card-collapse]{display:none}.card.maximized-card [data-lte-icon=maximize]{display:none}.card.maximized-card .card-footer,.card.maximized-card .card-header{border-radius:0!important}.card:not(.maximized-card) [data-lte-icon=minimize]{display:none}.card.collapsed-card [data-lte-icon=collapse]{display:none}.card.collapsed-card .card-body,.card.collapsed-card .card-footer{display:none}.card:not(.collapsed-card) [data-lte-icon=expand]{display:none}.card .nav.flex-column>li{margin:0;border-bottom:1px solid var(--bs-border-color-translucent)}.card .nav.flex-column>li:last-of-type{border-bottom:0}.card.height-control .card-body{max-height:300px;overflow:auto}.card .border-end{border-right:1px solid var(--bs-border-color-translucent)}.card .border-start{border-left:1px solid var(--bs-border-color-translucent)}.card.card-tabs:not(.card-outline)>.card-header{border-bottom:0}.card.card-tabs:not(.card-outline)>.card-header .nav-item:first-child .nav-link{border-left-color:transparent}.card.card-tabs.card-outline .nav-item{border-bottom:0}.card.card-tabs.card-outline .nav-item:first-child .nav-link{margin-left:0;border-left:0}.card.card-tabs .card-tools{margin:.3rem .5rem}.card.card-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}.card.card-outline-tabs{border-top:0}.card.card-outline-tabs .card-header .nav-item:first-child .nav-link{margin-left:0;border-left:0}.card.card-outline-tabs .card-header a{text-decoration:none;border-top:3px solid transparent}.card.card-outline-tabs .card-header a:hover{border-top:3px solid var(--bs-border-color)}.card.card-outline-tabs .card-header a.active:hover{margin-top:0}.card.card-outline-tabs .card-tools{margin:.5rem .5rem .3rem}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-outline-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}html.maximized-card{overflow:hidden}.card-body::after,.card-footer::after,.card-header::after{display:block;clear:both;content:""}.card-header{position:relative;padding:1rem 1rem;background-color:transparent;border-bottom:1px solid var(--bs-border-color-translucent);border-top-left-radius:.375rem;border-top-right-radius:.375rem}.collapsed-card .card-header{border-bottom:0}.card-header>.card-tools{float:right;margin-right:-.5rem}.card-header>.card-tools .input-group,.card-header>.card-tools .nav,.card-header>.card-tools .pagination{margin-top:-.4rem;margin-bottom:-.4rem}.card-header>.card-tools [data-bs-toggle=tooltip]{position:relative}.card-title{float:left;margin:0;font-size:1.1rem;font-weight:400}.btn-tool{--bs-btn-padding-x:.5rem;--bs-btn-padding-y:.25rem}.btn-tool:not(.btn-tool-custom){--bs-btn-color:var(--bs-tertiary-color);--bs-btn-bg:transparent;--bs-btn-box-shadow:none;--bs-btn-hover-color:var(--bs-secondary-color);--bs-btn-active-border-color:transparent}.btn-tool{margin:-1rem 0;font-size:.875rem}.bg-primary,.card-primary,.text-bg-primary{--lte-card-variant-bg:#0d6efd;--lte-card-variant-bg-rgb:13,110,253;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-secondary,.card-secondary,.text-bg-secondary{--lte-card-variant-bg:#6c757d;--lte-card-variant-bg-rgb:108,117,125;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-success,.card-success,.text-bg-success{--lte-card-variant-bg:#198754;--lte-card-variant-bg-rgb:25,135,84;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-info,.card-info,.text-bg-info{--lte-card-variant-bg:#0dcaf0;--lte-card-variant-bg-rgb:13,202,240;--lte-card-variant-color:#000;--lte-card-variant-color-rgb:0,0,0}.bg-warning,.card-warning,.text-bg-warning{--lte-card-variant-bg:#ffc107;--lte-card-variant-bg-rgb:255,193,7;--lte-card-variant-color:#000;--lte-card-variant-color-rgb:0,0,0}.bg-danger,.card-danger,.text-bg-danger{--lte-card-variant-bg:#dc3545;--lte-card-variant-bg-rgb:220,53,69;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.bg-light,.card-light,.text-bg-light{--lte-card-variant-bg:#f8f9fa;--lte-card-variant-bg-rgb:248,249,250;--lte-card-variant-color:#000;--lte-card-variant-color-rgb:0,0,0}.bg-dark,.card-dark,.text-bg-dark{--lte-card-variant-bg:#212529;--lte-card-variant-bg-rgb:33,37,41;--lte-card-variant-color:#fff;--lte-card-variant-color-rgb:255,255,255}.card-body>.table{margin-bottom:0}.card-body>.table>thead>tr>td,.card-body>.table>thead>tr>th{border-top-width:0}.table:not(.table-dark){color:inherit}.table.table-head-fixed thead tr:first-child th{position:sticky;top:0;z-index:10;background-color:#fff;border-bottom:0;box-shadow:inset 0 1px 0 var(--bs-border-color),inset 0 -1px 0 var(--bs-border-color)}.table.no-border,.table.no-border td,.table.no-border th{border:0}.table.text-center,.table.text-center td,.table.text-center th{text-align:center}.table.table-valign-middle tbody>tr>td,.table.table-valign-middle tbody>tr>th,.table.table-valign-middle thead>tr>td,.table.table-valign-middle thead>tr>th{vertical-align:middle}.card-body.p-0 .table tbody>tr>td:first-of-type,.card-body.p-0 .table tbody>tr>th:first-of-type,.card-body.p-0 .table tfoot>tr>td:first-of-type,.card-body.p-0 .table tfoot>tr>th:first-of-type,.card-body.p-0 .table thead>tr>td:first-of-type,.card-body.p-0 .table thead>tr>th:first-of-type{padding-left:1.5rem}.card-body.p-0 .table tbody>tr>td:last-of-type,.card-body.p-0 .table tbody>tr>th:last-of-type,.card-body.p-0 .table tfoot>tr>td:last-of-type,.card-body.p-0 .table tfoot>tr>th:last-of-type,.card-body.p-0 .table thead>tr>td:last-of-type,.card-body.p-0 .table thead>tr>th:last-of-type{padding-right:1.5rem}.small-box{border-radius:.375rem;box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2);position:relative;display:block;margin-bottom:1.25rem;--bs-link-color-rgb:none;--bs-link-hover-color-rgb:none;--bs-heading-color:none}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;z-index:10;display:block;padding:3px 0;text-align:center;background-color:rgba(0,0,0,.07)}.small-box>.small-box-footer:hover{background-color:rgba(0,0,0,.1)}.small-box .h3,.small-box h3{font-size:calc(1.345rem + 1.14vw)}@media (min-width:1200px){.small-box .h3,.small-box h3{font-size:2.2rem}}.small-box .h3,.small-box h3{padding:0;margin:0 0 10px;font-weight:700;white-space:nowrap}@media (min-width:992px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:calc(1.285rem + .42vw)}}@media (min-width:992px) and (min-width:1200px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:1.6rem}}@media (min-width:992px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:calc(1.285rem + .42vw)}}@media (min-width:992px) and (min-width:1200px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:1.6rem}}@media (min-width:1200px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:calc(1.345rem + 1.14vw)}}@media (min-width:1200px) and (min-width:1200px){.col-lg-2 .small-box .h3,.col-lg-2 .small-box h3,.col-md-2 .small-box .h3,.col-md-2 .small-box h3,.col-xl-2 .small-box .h3,.col-xl-2 .small-box h3{font-size:2.2rem}}@media (min-width:1200px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:calc(1.345rem + 1.14vw)}}@media (min-width:1200px) and (min-width:1200px){.col-lg-3 .small-box .h3,.col-lg-3 .small-box h3,.col-md-3 .small-box .h3,.col-md-3 .small-box h3,.col-xl-3 .small-box .h3,.col-xl-3 .small-box h3{font-size:2.2rem}}.small-box p{font-size:1rem}.small-box p>.small,.small-box p>small{display:block;margin-top:5px;font-size:.9rem;color:#f8f9fa}.small-box .h3,.small-box h3,.small-box p{z-index:5}.small-box .small-box-icon{position:absolute;top:15px;right:15px;z-index:0;height:70px;font-size:70px;color:rgba(0,0,0,.15);transition:transform .3s linear}@media (prefers-reduced-motion:reduce){.small-box .small-box-icon{transition:none}}.small-box:hover .small-box-icon{transform:scale(1.1)}@media (max-width:575.98px){.small-box{text-align:center}.small-box .small-box-icon{display:none}.small-box p{font-size:12px}}.info-box{box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2);border-radius:.375rem;position:relative;display:flex;width:100%;min-height:80px;padding:.5rem;margin-bottom:1rem;color:var(--bs-body-color);background-color:var(--bs-body-bg)}.info-box .progress{height:2px;margin:5px 0;background-color:rgba(var(--lte-card-variant-color-rgb),.125)}.info-box .progress .progress-bar{background-color:var(--lte-card-variant-color)}.info-box .info-box-icon{display:flex;align-items:center;justify-content:center;width:70px;font-size:1.875rem;text-align:center;border-radius:.375rem}.info-box .info-box-icon>img{max-width:100%}.info-box .info-box-content{display:flex;flex:1;flex-direction:column;justify-content:center;padding:0 10px;line-height:1.8}.info-box .info-box-number{display:block;margin-top:.25rem;font-weight:700}.info-box .info-box-text,.info-box .progress-description{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.info-box .info-box-more{display:block}.info-box .progress-description{margin:0}@media (min-width:768px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{display:none}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{display:none}}@media (min-width:992px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:.75rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:.75rem;display:block}}@media (min-width:1200px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:1rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:1rem;display:block}}.timeline{position:relative;padding:0;margin:0 0 45px}.timeline::before{border-radius:.375rem;position:absolute;top:0;bottom:0;left:31px;width:4px;margin:0;content:"";background-color:var(--bs-border-color)}.timeline>div::after,.timeline>div::before{display:table;content:""}.timeline>div{position:relative;margin-right:10px;margin-bottom:15px}.timeline>div>.timeline-item{box-shadow:0 0 1px rgba(var(--bs-body-color-rgb),.125),0 1px 3px rgba(var(--bs-body-color-rgb),.2);border-radius:.375rem;position:relative;padding:0;margin-top:0;margin-right:15px;margin-left:60px;color:var(--bs-body-color);background-color:var(--bs-body-bg)}.timeline>div>.timeline-item>.time{float:right;padding:10px;font-size:12px;color:var(--bs-secondary-color)}.timeline>div>.timeline-item>.timeline-header{padding:10px;margin:0;font-size:16px;line-height:1.1;color:var(--bs-secondary-color);border-bottom:1px solid var(--bs-border-color)}.timeline>div>.timeline-item>.timeline-header>a{font-weight:600;text-decoration:none}.timeline>div>.timeline-item>.timeline-body,.timeline>div>.timeline-item>.timeline-footer{padding:10px}.timeline>div>.timeline-item>.timeline-body>img{margin:10px}.timeline>div>.timeline-item>.timeline-body ol,.timeline>div>.timeline-item>.timeline-body ul,.timeline>div>.timeline-item>.timeline-body>dl{margin:0}.timeline>div .timeline-icon{position:absolute;top:0;left:18px;width:30px;height:30px;font-size:16px;line-height:30px;text-align:center;background-color:var(--bs-secondary-bg);border-radius:50%}.timeline>.time-label>span{border-radius:4px;display:inline-block;padding:5px;font-weight:600;background-color:var(--bs-body-bg)}.timeline-inverse>div>.timeline-item{box-shadow:none;background-color:var(--bs-tertiary-bg);border:1px solid var(--bs-border-color)}.timeline-inverse>div>.timeline-item>.timeline-header{border-bottom-color:var(--bs-border-color)}.direct-chat .card-body{position:relative;padding:0;overflow-x:hidden}.direct-chat.chat-pane-open .direct-chat-contacts{transform:translate(0,0)}.direct-chat.timestamp-light .direct-chat-timestamp{color:rgba(var(--bs-body-color-rgb),.65)}.direct-chat.timestamp-dark .direct-chat-timestamp{color:rgba(var(--bs-body-color-rgb),.9)}.direct-chat-messages{height:250px;padding:10px;overflow:auto;transform:translate(0,0)}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg::after{display:block;clear:both;content:""}.direct-chat-msg{margin-bottom:10px}.direct-chat-contacts,.direct-chat-messages{transition:transform .5s ease-in-out}@media (prefers-reduced-motion:reduce){.direct-chat-contacts,.direct-chat-messages{transition:none}}.direct-chat-text{border-radius:.5rem;position:relative;padding:5px 10px;margin:5px 0 0 50px;color:var(--bs-emphasis-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color)}.direct-chat-text::after,.direct-chat-text::before{position:absolute;top:15px;right:100%;width:0;height:0;pointer-events:none;content:" ";border:solid transparent;border-right-color:var(--bs-border-color)}.direct-chat-text::after{margin-top:-5px;border-width:5px}.direct-chat-text::before{margin-top:-6px;border-width:6px}.end .direct-chat-text{margin-right:50px;margin-left:0}.end .direct-chat-text::after,.end .direct-chat-text::before{right:auto;left:100%;border-right-color:transparent;border-left-color:var(--bs-border-color)}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.end .direct-chat-img{float:right}.direct-chat-infos{display:block;margin-bottom:2px;font-size:.875rem}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:rgba(var(--bs-body-color-rgb),.75)}.direct-chat-contacts-open .direct-chat-contacts{transform:translate(0,0)}.direct-chat-contacts{position:absolute;top:0;bottom:0;width:100%;height:250px;overflow:auto;color:var(--bs-body-bg);background-color:var(--bs-body-color);transform:translate(101%,0)}.direct-chat-contacts-light{background-color:var(--bs-light-bg-subtle)}.direct-chat-contacts-light .contacts-list-name{color:var(--bs-body-color)}.direct-chat-contacts-light .contacts-list-date{color:var(--bs-secondary-color)}.direct-chat-contacts-light .contacts-list-msg{color:var(--bs-secondary-color)}.contacts-list{padding-left:0;list-style:none}.contacts-list>li::after{display:block;clear:both;content:""}.contacts-list>li{padding:10px;margin:0;text-decoration:none;border-bottom:1px solid rgba(0,0,0,.2)}.contacts-list>li:last-of-type{border-bottom:0}.contacts-list>li a{text-decoration:none}.contacts-list-img{border-radius:50%;float:left;width:40px}.contacts-list-info{margin-left:45px;color:var(--bs-body-bg)}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:.875rem}.contacts-list-date{font-weight:400;color:var(--bs-secondary-bg)}.contacts-list-msg{color:var(--bs-secondary-bg)}.end>.direct-chat-text{color:var(--lte-direct-chat-color);background-color:var(--lte-direct-chat-bg);border-color:var(--lte-direct-chat-bg)}.end>.direct-chat-text::after,.end>.direct-chat-text::before{border-left-color:var(--lte-direct-chat-bg)}.direct-chat-primary{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#0d6efd}.direct-chat-secondary{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#6c757d}.direct-chat-success{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#198754}.direct-chat-info{--lte-direct-chat-color:#000;--lte-direct-chat-bg:#0dcaf0}.direct-chat-warning{--lte-direct-chat-color:#000;--lte-direct-chat-bg:#ffc107}.direct-chat-danger{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#dc3545}.direct-chat-light{--lte-direct-chat-color:#000;--lte-direct-chat-bg:#f8f9fa}.direct-chat-dark{--lte-direct-chat-color:#fff;--lte-direct-chat-bg:#212529}.toast-primary{--bs-toast-header-color:#fff;--bs-toast-header-bg:#0d6efd;--bs-toast-header-border-color:#0d6efd;--bs-toast-border-color:#0d6efd;--bs-toast-bg:var(--bs-primary-bg-subtle)}.toast-primary .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-secondary{--bs-toast-header-color:#fff;--bs-toast-header-bg:#6c757d;--bs-toast-header-border-color:#6c757d;--bs-toast-border-color:#6c757d;--bs-toast-bg:var(--bs-secondary-bg-subtle)}.toast-secondary .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-success{--bs-toast-header-color:#fff;--bs-toast-header-bg:#198754;--bs-toast-header-border-color:#198754;--bs-toast-border-color:#198754;--bs-toast-bg:var(--bs-success-bg-subtle)}.toast-success .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-info{--bs-toast-header-color:#000;--bs-toast-header-bg:#0dcaf0;--bs-toast-header-border-color:#0dcaf0;--bs-toast-border-color:#0dcaf0;--bs-toast-bg:var(--bs-info-bg-subtle)}.toast-warning{--bs-toast-header-color:#000;--bs-toast-header-bg:#ffc107;--bs-toast-header-border-color:#ffc107;--bs-toast-border-color:#ffc107;--bs-toast-bg:var(--bs-warning-bg-subtle)}.toast-danger{--bs-toast-header-color:#fff;--bs-toast-header-bg:#dc3545;--bs-toast-header-border-color:#dc3545;--bs-toast-border-color:#dc3545;--bs-toast-bg:var(--bs-danger-bg-subtle)}.toast-danger .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast-light{--bs-toast-header-color:#000;--bs-toast-header-bg:#f8f9fa;--bs-toast-header-border-color:#f8f9fa;--bs-toast-border-color:#f8f9fa;--bs-toast-bg:var(--bs-light-bg-subtle)}.toast-dark{--bs-toast-header-color:#fff;--bs-toast-header-bg:#212529;--bs-toast-header-border-color:#212529;--bs-toast-border-color:#212529;--bs-toast-bg:var(--bs-dark-bg-subtle)}.toast-dark .btn-close{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}[data-bs-theme=dark] .toast-info .btn-close{--bs-btn-close-white-filter:none}[data-bs-theme=dark] .toast-warning .btn-close{--bs-btn-close-white-filter:none}[data-bs-theme=dark] .toast-light .btn-close{--bs-btn-close-white-filter:none}.login-logo,.register-logo{margin-bottom:.9rem;font-size:2.1rem;font-weight:300;text-align:center}.login-logo a,.register-logo a{color:var(--bs-secondary-color);text-decoration:none}.login-page,.register-page{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}.login-box,.register-box{width:360px}@media (max-width:576px){.login-box,.register-box{width:90%;margin-top:.5rem}}.login-box .card,.register-box .card{margin-bottom:0}.login-card-body,.register-card-body{padding:20px;color:var(--bs-secondary-color);background-color:var(--bs-body-bg);border-top:0}.login-card-body .input-group .form-control:focus,.register-card-body .input-group .form-control:focus{box-shadow:none}.login-card-body .input-group .form-control:focus~.input-group-append .input-group-text,.login-card-body .input-group .form-control:focus~.input-group-prepend .input-group-text,.register-card-body .input-group .form-control:focus~.input-group-append .input-group-text,.register-card-body .input-group .form-control:focus~.input-group-prepend .input-group-text{border-color:rgb(134,182.5,254)}.login-card-body .input-group .form-control.is-valid:focus,.register-card-body .input-group .form-control.is-valid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text,.login-card-body .input-group .form-control.is-valid~.input-group-prepend .input-group-text,.register-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-valid~.input-group-prepend .input-group-text{border-color:#198754}.login-card-body .input-group .form-control.is-invalid:focus,.register-card-body .input-group .form-control.is-invalid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text{border-color:#dc3545}.login-card-body .input-group .input-group-text,.register-card-body .input-group .input-group-text{color:var(--bs-secondary-color);background-color:transparent;border-top-right-radius:.375rem;border-bottom-right-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.login-card-body .input-group .input-group-text,.register-card-body .input-group .input-group-text{transition:none}}.login-box-msg,.register-box-msg{padding:0 20px 20px;margin:0;text-align:center}.social-auth-links{margin:10px 0}.lockscreen .lockscreen-name{font-weight:600;text-align:center}.lockscreen .lockscreen-logo{margin-bottom:25px;font-size:35px;font-weight:300;text-align:center}.lockscreen .lockscreen-logo a{color:var(--bs-emphasis-color);text-decoration:none}.lockscreen .lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-item{position:relative;width:290px;padding:0;margin:10px auto 30px;background-color:var(--bs-body-bg);border-radius:4px}.lockscreen .lockscreen-image{position:absolute;top:-25px;left:-10px;z-index:10;padding:5px;background-color:var(--bs-body-bg);border-radius:50%}.lockscreen .lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen .lockscreen-credentials{margin-left:70px}.lockscreen .lockscreen-credentials .form-control{border:0}.lockscreen .lockscreen-credentials .btn{padding:0 10px;border:0}.lockscreen .lockscreen-footer{margin-top:10px}.img-size-32,.img-size-50,.img-size-64{height:auto}.img-size-64{width:64px}.img-size-50{width:50px}.img-size-32{width:32px}.skip-link{position:absolute;top:-40px;left:6px;z-index:999999;padding:8px 16px;font-weight:600;color:var(--bs-white);text-decoration:none;background:var(--bs-primary)}.skip-link:focus{top:0;outline:3px solid var(--bs-warning);outline-offset:2px}.skip-link:hover{color:var(--bs-white);text-decoration:none;background:var(--bs-primary-emphasis)}.focus-enhanced:focus{outline:3px solid var(--bs-focus-ring-color,#0d6efd);outline-offset:2px;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}@media (prefers-contrast:high){.card{border:2px solid}.btn{border-width:2px}.nav-link{border:1px solid transparent}.nav-link:focus,.nav-link:hover{border-color:currentcolor}}@media (prefers-reduced-motion:reduce){*,::after,::before{transition-duration:0s!important;animation-duration:0s!important;animation-iteration-count:1!important;scroll-behavior:auto!important}.fade{opacity:1!important;transition:none!important}.collapse{transition:none!important}.modal.fade .modal-dialog{transform:none!important}}.sr-only{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}.sr-only-focusable:focus{position:static!important;width:auto!important;height:auto!important;padding:inherit!important;margin:inherit!important;overflow:visible!important;clip:auto!important;white-space:normal!important}.focus-trap:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.text-accessible-primary{color:#003d82}.text-accessible-success{color:#0f5132}.text-accessible-danger{color:#842029}.text-accessible-warning{color:#664d03}.live-region{position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden}.live-region.live-region-visible{position:static;left:auto;width:auto;height:auto;overflow:visible}.form-control.is-invalid{border-color:var(--bs-danger)}.form-control.is-invalid:focus{border-color:var(--bs-danger);box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.invalid-feedback{display:block;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-danger)}.invalid-feedback[role=alert]{font-weight:600}.touch-target{min-width:44px;min-height:44px}.touch-target.touch-target-small{min-width:24px;min-height:24px}.table-accessible th{font-weight:600;background-color:var(--bs-secondary-bg)}.table-accessible th[scope=col]{border-bottom:2px solid var(--bs-border-color)}.table-accessible th[scope=row]{border-right:2px solid var(--bs-border-color)}.table-accessible caption{padding:.75rem;font-weight:600;color:var(--bs-secondary);text-align:left;caption-side:top}nav[role=navigation]:not([aria-label]):not([aria-labelledby])::before{position:absolute;left:-10000px;content:"Navigation"}fieldset{padding:1rem;margin-bottom:1rem;border:1px solid var(--bs-border-color)}fieldset legend{padding:0 .5rem;margin-bottom:.5rem;font-size:1.1em;font-weight:600}.loading[aria-busy=true]{position:relative;pointer-events:none}.loading[aria-busy=true]::after{position:absolute;top:50%;left:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;content:"";border:2px solid var(--bs-primary);border-top-color:transparent;animation:spin 1s linear infinite}@media (prefers-reduced-motion:reduce){.loading[aria-busy=true]::after{border-top-color:var(--bs-primary);animation:none}}@keyframes spin{to{transform:rotate(360deg)}}[data-bs-theme=dark] .text-accessible-primary{color:#6ea8fe}[data-bs-theme=dark] .text-accessible-success{color:#75b798}[data-bs-theme=dark] .text-accessible-danger{color:#f1aeb5}[data-bs-theme=dark] .text-accessible-warning{color:#ffda6a}@media print{.btn,.nav-link,.skip-link{color:#000!important;background:0 0!important;border:1px solid #000!important}a[href^=http]::after{font-size:.8em;content:" (" attr(href) ")"}} /*# sourceMappingURL=adminlte.min.css.map */ \ No newline at end of file diff --git a/css/bootstrap-icons.min.css b/css/bootstrap-icons.min.css index 3fd4779..4cf5f17 100644 --- a/css/bootstrap-icons.min.css +++ b/css/bootstrap-icons.min.css @@ -1,5 +1,5 @@ -/*! - * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) - * Copyright 2019-2024 The Bootstrap Authors - * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) +/*! + * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) */@font-face{font-display:block;font-family:bootstrap-icons;src:url("bootstrap-icons.woff2") format("woff2"),url("bootstrap-icons.woff") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"}.bi-alphabet-uppercase::before{content:"\f2a5"}.bi-alphabet::before{content:"\f68a"}.bi-amazon::before{content:"\f68d"}.bi-arrows-collapse-vertical::before{content:"\f690"}.bi-arrows-expand-vertical::before{content:"\f695"}.bi-arrows-vertical::before{content:"\f698"}.bi-arrows::before{content:"\f6a2"}.bi-ban-fill::before{content:"\f6a3"}.bi-ban::before{content:"\f6b6"}.bi-bing::before{content:"\f6c2"}.bi-cake::before{content:"\f6e0"}.bi-cake2::before{content:"\f6ed"}.bi-cookie::before{content:"\f6ee"}.bi-copy::before{content:"\f759"}.bi-crosshair::before{content:"\f769"}.bi-crosshair2::before{content:"\f794"}.bi-emoji-astonished-fill::before{content:"\f795"}.bi-emoji-astonished::before{content:"\f79a"}.bi-emoji-grimace-fill::before{content:"\f79b"}.bi-emoji-grimace::before{content:"\f7a0"}.bi-emoji-grin-fill::before{content:"\f7a1"}.bi-emoji-grin::before{content:"\f7a6"}.bi-emoji-surprise-fill::before{content:"\f7a7"}.bi-emoji-surprise::before{content:"\f7ac"}.bi-emoji-tear-fill::before{content:"\f7ad"}.bi-emoji-tear::before{content:"\f7b2"}.bi-envelope-arrow-down-fill::before{content:"\f7b3"}.bi-envelope-arrow-down::before{content:"\f7b8"}.bi-envelope-arrow-up-fill::before{content:"\f7b9"}.bi-envelope-arrow-up::before{content:"\f7be"}.bi-feather::before{content:"\f7bf"}.bi-feather2::before{content:"\f7c4"}.bi-floppy-fill::before{content:"\f7c5"}.bi-floppy::before{content:"\f7d8"}.bi-floppy2-fill::before{content:"\f7d9"}.bi-floppy2::before{content:"\f7e4"}.bi-gitlab::before{content:"\f7e5"}.bi-highlighter::before{content:"\f7f8"}.bi-marker-tip::before{content:"\f802"}.bi-nvme-fill::before{content:"\f803"}.bi-nvme::before{content:"\f80c"}.bi-opencollective::before{content:"\f80d"}.bi-pci-card-network::before{content:"\f8cd"}.bi-pci-card-sound::before{content:"\f8ce"}.bi-radar::before{content:"\f8cf"}.bi-send-arrow-down-fill::before{content:"\f8d0"}.bi-send-arrow-down::before{content:"\f8d1"}.bi-send-arrow-up-fill::before{content:"\f8d2"}.bi-send-arrow-up::before{content:"\f8d3"}.bi-sim-slash-fill::before{content:"\f8d4"}.bi-sim-slash::before{content:"\f8d5"}.bi-sourceforge::before{content:"\f8d6"}.bi-substack::before{content:"\f8d7"}.bi-threads-fill::before{content:"\f8d8"}.bi-threads::before{content:"\f8d9"}.bi-transparency::before{content:"\f8da"}.bi-twitter-x::before{content:"\f8db"}.bi-type-h4::before{content:"\f8dc"}.bi-type-h5::before{content:"\f8dd"}.bi-type-h6::before{content:"\f8de"}.bi-backpack-fill::before{content:"\f8df"}.bi-backpack::before{content:"\f8e0"}.bi-backpack2-fill::before{content:"\f8e1"}.bi-backpack2::before{content:"\f8e2"}.bi-backpack3-fill::before{content:"\f8e3"}.bi-backpack3::before{content:"\f8e4"}.bi-backpack4-fill::before{content:"\f8e5"}.bi-backpack4::before{content:"\f8e6"}.bi-brilliance::before{content:"\f8e7"}.bi-cake-fill::before{content:"\f8e8"}.bi-cake2-fill::before{content:"\f8e9"}.bi-duffle-fill::before{content:"\f8ea"}.bi-duffle::before{content:"\f8eb"}.bi-exposure::before{content:"\f8ec"}.bi-gender-neuter::before{content:"\f8ed"}.bi-highlights::before{content:"\f8ee"}.bi-luggage-fill::before{content:"\f8ef"}.bi-luggage::before{content:"\f8f0"}.bi-mailbox-flag::before{content:"\f8f1"}.bi-mailbox2-flag::before{content:"\f8f2"}.bi-noise-reduction::before{content:"\f8f3"}.bi-passport-fill::before{content:"\f8f4"}.bi-passport::before{content:"\f8f5"}.bi-person-arms-up::before{content:"\f8f6"}.bi-person-raised-hand::before{content:"\f8f7"}.bi-person-standing-dress::before{content:"\f8f8"}.bi-person-standing::before{content:"\f8f9"}.bi-person-walking::before{content:"\f8fa"}.bi-person-wheelchair::before{content:"\f8fb"}.bi-shadows::before{content:"\f8fc"}.bi-suitcase-fill::before{content:"\f8fd"}.bi-suitcase-lg-fill::before{content:"\f8fe"}.bi-suitcase-lg::before{content:"\f8ff"}.bi-suitcase::before{content:"\f900"}.bi-suitcase2-fill::before{content:"\f901"}.bi-suitcase2::before{content:"\f902"}.bi-vignette::before{content:"\f903"}.bi-bluesky::before{content:"\f7f9"}.bi-tux::before{content:"\f904"}.bi-beaker-fill::before{content:"\f905"}.bi-beaker::before{content:"\f906"}.bi-flask-fill::before{content:"\f907"}.bi-flask-florence-fill::before{content:"\f908"}.bi-flask-florence::before{content:"\f909"}.bi-flask::before{content:"\f90a"}.bi-leaf-fill::before{content:"\f90b"}.bi-leaf::before{content:"\f90c"}.bi-measuring-cup-fill::before{content:"\f90d"}.bi-measuring-cup::before{content:"\f90e"}.bi-unlock2-fill::before{content:"\f90f"}.bi-unlock2::before{content:"\f910"}.bi-battery-low::before{content:"\f911"}.bi-anthropic::before{content:"\f912"}.bi-apple-music::before{content:"\f913"}.bi-claude::before{content:"\f914"}.bi-openai::before{content:"\f915"}.bi-perplexity::before{content:"\f916"}.bi-css::before{content:"\f917"}.bi-javascript::before{content:"\f918"}.bi-typescript::before{content:"\f919"}.bi-fork-knife::before{content:"\f91a"}.bi-globe-americas-fill::before{content:"\f91b"}.bi-globe-asia-australia-fill::before{content:"\f91c"}.bi-globe-central-south-asia-fill::before{content:"\f91d"}.bi-globe-europe-africa-fill::before{content:"\f91e"} \ No newline at end of file diff --git a/helper.php b/helper.php index 485af29..656b704 100644 --- a/helper.php +++ b/helper.php @@ -1,96 +1,96 @@ - strlen($adresse) * 8) { - return false; - } - } else { - $bits = strlen($adresse) * 8; - } - $ganzeBytes = intdiv($bits, 8); - $restBits = $bits % 8; - if ($ganzeBytes > 0 && strncmp($adresse, $netz, $ganzeBytes) !== 0) { - return false; - } - if ($restBits === 0) { - return true; - } - $maske = chr((0xFF << (8 - $restBits)) & 0xFF); - return ($adresse[$ganzeBytes] & $maske) === ($netz[$ganzeBytes] & $maske); -} - -/** - * Kommt die Anfrage aus dem Heimnetz? Nur dann gilt sie ohne Anmeldung als - * berechtigt. - * - * REMOTE_ADDR stammt aus der TCP-Verbindung und ist nicht faelschbar. - * Weiterleitungs-Header wie X-Forwarded-For duerfen hier bewusst NICHT - * herangezogen werden - sie kann jeder Aufrufer frei setzen. - */ -function isLocal() -{ - $_SESSION["local"] = false; - $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; - $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : ''; - - if (filter_var($remote, FILTER_VALIDATE_IP) === false) { - return false; - } - - // Auf IPv6 abgebildete IPv4-Adressen (::ffff:192.168.179.44) auf ihre - // IPv4-Form zurueckfuehren. Ein Server mit Dual-Stack-Socket meldet - // LAN-Clients so - ohne diesen Schritt faellt das Heimnetz durch. - if (preg_match('/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i', $remote, $treffer)) { - $remote = $treffer[1]; - } - - $lokal = false; - foreach (LOCAL_NETWORKS as $netz) { - if (ipInNetwork($remote, $netz)) { - $lokal = true; - break; - } - } - - // Gleiches IPv6-/64 wie der Server. Das Praefix vergibt der Provider und - // es kann sich aendern, deshalb aus SERVER_ADDR abgeleitet statt fest. - if (!$lokal - && filter_var($remote, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false - && filter_var($server, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) { - $lokal = substr(inet_pton($remote), 0, 8) === substr(inet_pton($server), 0, 8); - } - - $_SESSION["local"] = $lokal; - return $lokal; -} - + strlen($adresse) * 8) { + return false; + } + } else { + $bits = strlen($adresse) * 8; + } + $ganzeBytes = intdiv($bits, 8); + $restBits = $bits % 8; + if ($ganzeBytes > 0 && strncmp($adresse, $netz, $ganzeBytes) !== 0) { + return false; + } + if ($restBits === 0) { + return true; + } + $maske = chr((0xFF << (8 - $restBits)) & 0xFF); + return ($adresse[$ganzeBytes] & $maske) === ($netz[$ganzeBytes] & $maske); +} + +/** + * Kommt die Anfrage aus dem Heimnetz? Nur dann gilt sie ohne Anmeldung als + * berechtigt. + * + * REMOTE_ADDR stammt aus der TCP-Verbindung und ist nicht faelschbar. + * Weiterleitungs-Header wie X-Forwarded-For duerfen hier bewusst NICHT + * herangezogen werden - sie kann jeder Aufrufer frei setzen. + */ +function isLocal() +{ + $_SESSION["local"] = false; + $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; + $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : ''; + + if (filter_var($remote, FILTER_VALIDATE_IP) === false) { + return false; + } + + // Auf IPv6 abgebildete IPv4-Adressen (::ffff:192.168.179.44) auf ihre + // IPv4-Form zurueckfuehren. Ein Server mit Dual-Stack-Socket meldet + // LAN-Clients so - ohne diesen Schritt faellt das Heimnetz durch. + if (preg_match('/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i', $remote, $treffer)) { + $remote = $treffer[1]; + } + + $lokal = false; + foreach (LOCAL_NETWORKS as $netz) { + if (ipInNetwork($remote, $netz)) { + $lokal = true; + break; + } + } + + // Gleiches IPv6-/64 wie der Server. Das Praefix vergibt der Provider und + // es kann sich aendern, deshalb aus SERVER_ADDR abgeleitet statt fest. + if (!$lokal + && filter_var($remote, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false + && filter_var($server, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) { + $lokal = substr(inet_pton($remote), 0, 8) === substr(inet_pton($server), 0, 8); + } + + $_SESSION["local"] = $lokal; + return $lokal; +} + /** * Kleine HTTP-GET-Anfrage im Heimnetz, Antwort als JSON-Array. * Ersetzt die frueher in carOG.php, heater.php und AutoAction.php je einzeln @@ -106,43 +106,43 @@ function httpGetJson($url){ return json_decode($response, true); } -function checkLogin(){ - $mysql = new mysqli($GLOBALS["mysql_server"],$GLOBALS["mysql_user"],$GLOBALS["mysql_pass"],$GLOBALS["mysql_db"]); - if(isLocal()){ - $_SESSION["local"] =true; - return true; - } - if(isset($_SESSION["authKey"])){ - $res = mysqli_query($mysql,"SELECT id FROM users WHERE lastAuth > DATE_SUB(NOW(), INTERVAL 2 DAY) AND authKey = '".mysqli_real_escape_string($mysql,$_SESSION["authKey"])."' AND name = '".mysqli_real_escape_string($mysql,$_SESSION["user"])."';"); - if(!$res){ - echo mysqli_error($mysql); - return false; - } - if(mysqli_num_rows($res) == 1){ - return isset($_SESSION["Logged"]); - } - }else{ - return false; - } -} - -function checkAdduser(){ - $mysql = new mysqli($GLOBALS["mysql_server"],$GLOBALS["mysql_user"],$GLOBALS["mysql_pass"],$GLOBALS["mysql_db"]); - if(!mysqli_query($mysql,"DELETE FROM addUser WHERE datetime < DATE_SUB(NOW(), INTERVAL 1 MINUTE);")){ - echo mysqli_error($mysql); - } - $result = mysqli_query($mysql,"SELECT * FROM addUser WHERE accesskey='".mysqli_real_escape_string($mysql,$_GET["addUser"])."';"); - if(!$result){ - return false; - } - if ($result->num_rows > 0) { - // Freigabe fuer die Passkey-Registrierung vermerken. authServer.php - // prueft dieses Merkmal; ohne das waeren getCreateArgs/processCreate - // direkt aufrufbar und der Einmalschluessel wirkungslos. - $_SESSION["mayRegister"] = time(); - return true; - } - return false; -} - +function checkLogin(){ + $mysql = new mysqli($GLOBALS["mysql_server"],$GLOBALS["mysql_user"],$GLOBALS["mysql_pass"],$GLOBALS["mysql_db"]); + if(isLocal()){ + $_SESSION["local"] =true; + return true; + } + if(isset($_SESSION["authKey"])){ + $res = mysqli_query($mysql,"SELECT id FROM users WHERE lastAuth > DATE_SUB(NOW(), INTERVAL 2 DAY) AND authKey = '".mysqli_real_escape_string($mysql,$_SESSION["authKey"])."' AND name = '".mysqli_real_escape_string($mysql,$_SESSION["user"])."';"); + if(!$res){ + echo mysqli_error($mysql); + return false; + } + if(mysqli_num_rows($res) == 1){ + return isset($_SESSION["Logged"]); + } + }else{ + return false; + } +} + +function checkAdduser(){ + $mysql = new mysqli($GLOBALS["mysql_server"],$GLOBALS["mysql_user"],$GLOBALS["mysql_pass"],$GLOBALS["mysql_db"]); + if(!mysqli_query($mysql,"DELETE FROM addUser WHERE datetime < DATE_SUB(NOW(), INTERVAL 1 MINUTE);")){ + echo mysqli_error($mysql); + } + $result = mysqli_query($mysql,"SELECT * FROM addUser WHERE accesskey='".mysqli_real_escape_string($mysql,$_GET["addUser"])."';"); + if(!$result){ + return false; + } + if ($result->num_rows > 0) { + // Freigabe fuer die Passkey-Registrierung vermerken. authServer.php + // prueft dieses Merkmal; ohne das waeren getCreateArgs/processCreate + // direkt aufrufbar und der Einmalschluessel wirkungslos. + $_SESSION["mayRegister"] = time(); + return true; + } + return false; +} + ?> \ No newline at end of file diff --git a/js/adminlte.min.js b/js/adminlte.min.js index 8e6dcbd..3ae89fb 100644 --- a/js/adminlte.min.js +++ b/js/adminlte.min.js @@ -1,7 +1,7 @@ -/*! - * AdminLTE v4.0.0-rc5 (https://adminlte.io) - * Copyright 2014-2025 Colorlib - * Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE) - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).adminlte={})}(this,function(e){"use strict";const t=[],n=e=>{"loading"===document.readyState?(t.length||document.addEventListener("DOMContentLoaded",()=>{for(const e of t)e()}),t.push(e)):e()},i=(e,t=500)=>{e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.boxSizing="border-box",e.style.height=`${e.offsetHeight}px`,e.style.overflow="hidden",globalThis.setTimeout(()=>{e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0"},1),globalThis.setTimeout(()=>{e.style.display="none",e.style.removeProperty("height"),e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")},t)},o=(e,t=500)=>{e.style.removeProperty("display");let{display:n}=globalThis.getComputedStyle(e);"none"===n&&(n="block"),e.style.display=n;const i=e.offsetHeight;e.style.overflow="hidden",e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0",globalThis.setTimeout(()=>{e.style.boxSizing="border-box",e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.height=`${i}px`,e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom")},1),globalThis.setTimeout(()=>{e.style.removeProperty("height"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")},t)},s="hold-transition";class a{_element;constructor(e){this._element=e}holdTransition(){let e;window.addEventListener("resize",()=>{document.body.classList.add(s),clearTimeout(e),e=setTimeout(()=>{document.body.classList.remove(s)},400)})}}n(()=>{new a(document.body).holdTransition(),setTimeout(()=>{document.body.classList.add("app-loaded")},400)});const r=".lte.card-widget",l=`collapsed${r}`,c=`expanded${r}`,d=`remove${r}`,u=`maximized${r}`,m=`minimized${r}`,h="card",p="collapsed-card",g="collapsing-card",y="expanding-card",v="was-collapsed",b="maximized-card",f='[data-lte-toggle="card-remove"]',E='[data-lte-toggle="card-collapse"]',_='[data-lte-toggle="card-maximize"]',S=`.${h}`,L=".card-body",w=".card-footer",A={animationSpeed:500,collapseTrigger:E,removeTrigger:f,maximizeTrigger:_};class k{_element;_parent;_clone;_config;constructor(e,t){this._element=e,this._parent=e.closest(S),e.classList.contains(h)&&(this._parent=e),this._config={...A,...t}}collapse(){const e=new Event(l);if(this._parent){this._parent.classList.add(g);const e=this._parent?.querySelectorAll(`${L}, ${w}`);e.forEach(e=>{e instanceof HTMLElement&&i(e,this._config.animationSpeed)}),setTimeout(()=>{this._parent&&(this._parent.classList.add(p),this._parent.classList.remove(g))},this._config.animationSpeed)}this._element?.dispatchEvent(e)}expand(){const e=new Event(c);if(this._parent){this._parent.classList.add(y);const e=this._parent?.querySelectorAll(`${L}, ${w}`);e.forEach(e=>{e instanceof HTMLElement&&o(e,this._config.animationSpeed)}),setTimeout(()=>{this._parent&&this._parent.classList.remove(p,y)},this._config.animationSpeed)}this._element?.dispatchEvent(e)}remove(){const e=new Event(d);this._parent&&i(this._parent,this._config.animationSpeed),this._element?.dispatchEvent(e)}toggle(){this._parent?.classList.contains(p)?this.expand():this.collapse()}maximize(){const e=new Event(u);this._parent&&(this._parent.style.height=`${this._parent.offsetHeight}px`,this._parent.style.width=`${this._parent.offsetWidth}px`,this._parent.style.transition="all .15s",setTimeout(()=>{const e=document.querySelector("html");e&&e.classList.add(b),this._parent&&(this._parent.classList.add(b),this._parent.classList.contains(p)&&this._parent.classList.add(v))},150)),this._element?.dispatchEvent(e)}minimize(){const e=new Event(m);this._parent&&(this._parent.style.height="auto",this._parent.style.width="auto",this._parent.style.transition="all .15s",setTimeout(()=>{const e=document.querySelector("html");e&&e.classList.remove(b),this._parent&&(this._parent.classList.remove(b),this._parent?.classList.contains(v)&&this._parent.classList.remove(v))},10)),this._element?.dispatchEvent(e)}toggleMaximize(){this._parent?.classList.contains(b)?this.minimize():this.maximize()}}n(()=>{document.querySelectorAll(E).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new k(t,A).toggle()})}),document.querySelectorAll(f).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new k(t,A).remove()})}),document.querySelectorAll(_).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new k(t,A).toggleMaximize()})})});const x=".lte.treeview",q=`expanded${x}`,T=`collapsed${x}`,$=`load${x}`,M="menu-open",D=".nav-item",N=".nav-treeview",P={animationSpeed:300,accordion:!0};class F{_element;_config;constructor(e,t){this._element=e,this._config={...P,...t}}open(){const e=new Event(q);if(this._config.accordion){const e=this._element.parentElement?.querySelectorAll(`${D}.${M}`);e?.forEach(e=>{if(e!==this._element.parentElement){e.classList.remove(M);const t=e?.querySelector(N);t&&i(t,this._config.animationSpeed)}})}this._element.classList.add(M);const t=this._element?.querySelector(N);t&&o(t,this._config.animationSpeed),this._element.dispatchEvent(e)}close(){const e=new Event(T);this._element.classList.remove(M);const t=this._element?.querySelector(N);t&&i(t,this._config.animationSpeed),this._element.dispatchEvent(e)}toggle(){this._element.classList.contains(M)?this.close():this.open()}}n(()=>{document.querySelectorAll(`${D}.${M}`).forEach(e=>{const t=e.querySelector(N);if(t){o(t,0);const n=new Event($);e.dispatchEvent(n)}}),document.querySelectorAll('[data-lte-toggle="treeview"]').forEach(e=>{e.addEventListener("click",e=>{const t=e.target,n=t.closest(D),i=t.closest(".nav-link"),o=e.currentTarget;if("#"!==t?.getAttribute("href")&&"#"!==i?.getAttribute("href")||e.preventDefault(),n){const e=o.dataset.accordion,t=o.dataset.animationSpeed,i={accordion:void 0===e?P.accordion:"true"===e,animationSpeed:void 0===t?P.animationSpeed:Number(t)};new F(n,i).toggle()}})})});const R=".lte.direct-chat",C=`expanded${R}`,z=`collapsed${R}`,B="direct-chat-contacts-open";class H{_element;constructor(e){this._element=e}toggle(){if(this._element.classList.contains(B)){const e=new Event(z);this._element.classList.remove(B),this._element.dispatchEvent(e)}else{const e=new Event(C);this._element.classList.add(B),this._element.dispatchEvent(e)}}}n(()=>{document.querySelectorAll('[data-lte-toggle="chat-pane"]').forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target.closest(".direct-chat");t&&new H(t).toggle()})})});const K=".lte.fullscreen",O=`maximized${K}`,W=`minimized${K}`,I='[data-lte-toggle="fullscreen"]',j='[data-lte-icon="maximize"]',U='[data-lte-icon="minimize"]';class V{_element;_config;constructor(e,t){this._element=e,this._config=t}inFullScreen(){const e=new Event(O),t=document.querySelector(j),n=document.querySelector(U);document.documentElement.requestFullscreen(),t&&(t.style.display="none"),n&&(n.style.display="block"),this._element.dispatchEvent(e)}outFullscreen(){const e=new Event(W),t=document.querySelector(j),n=document.querySelector(U);document.exitFullscreen(),t&&(t.style.display="block"),n&&(n.style.display="none"),this._element.dispatchEvent(e)}toggleFullScreen(){document.fullscreenEnabled&&(document.fullscreenElement?this.outFullscreen():this.inFullScreen())}}n(()=>{document.querySelectorAll(I).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target.closest(I);t&&new V(t,void 0).toggleFullScreen()})})});const G=".lte.push-menu",J=`open${G}`,Q=`collapse${G}`,X="sidebar-mini",Y="sidebar-collapse",Z="sidebar-open",ee="sidebar-expand",te=`[class*="${ee}"]`,ne='[data-lte-toggle="sidebar"]',ie={sidebarBreakpoint:992};class oe{_element;_config;constructor(e,t){this._element=e,this._config={...ie,...t}}menusClose(){document.querySelectorAll(".nav-treeview").forEach(e=>{e.style.removeProperty("display"),e.style.removeProperty("height")});const e=document.querySelector(".sidebar-menu"),t=e?.querySelectorAll(".nav-item");t&&t.forEach(e=>{e.classList.remove("menu-open")})}expand(){const e=new Event(J);document.body.classList.remove(Y),document.body.classList.add(Z),this._element.dispatchEvent(e)}collapse(){const e=new Event(Q);document.body.classList.remove(Z),document.body.classList.add(Y),this._element.dispatchEvent(e)}addSidebarBreakPoint(){const e=document.querySelector(te)?.classList??[],t=Array.from(e).find(e=>e.startsWith(ee))??"",n=document.getElementsByClassName(t)[0],i=globalThis.getComputedStyle(n,"::before").getPropertyValue("content");this._config={...this._config,sidebarBreakpoint:Number(i.replace(/[^\d.-]/g,""))};const o=document.body.classList.contains(Z);window.innerWidth<=this._config.sidebarBreakpoint?o||this.collapse():(document.body.classList.contains(X)||this.expand(),document.body.classList.contains(X)&&document.body.classList.contains(Y)&&this.collapse())}toggle(){document.body.classList.contains(Y)?this.expand():this.collapse()}init(){this.addSidebarBreakPoint()}}n(()=>{const e=document?.querySelector(".app-sidebar");if(e){const t=new oe(e,ie);t.init(),window.addEventListener("resize",()=>{t.init()})}const t=document.createElement("div");t.className="sidebar-overlay",document.querySelector(".app-wrapper")?.append(t);let n=!1;t.addEventListener("touchstart",()=>{n=!1},{passive:!0}),t.addEventListener("touchmove",()=>{n=!0},{passive:!0}),t.addEventListener("touchend",e=>{if(!n){e.preventDefault();const t=e.currentTarget;new oe(t,ie).collapse()}n=!1},{passive:!1}),t.addEventListener("click",e=>{e.preventDefault();const t=e.currentTarget;new oe(t,ie).collapse()}),document.querySelectorAll(ne).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();let t=e.currentTarget;"sidebar"!==t?.dataset.lteToggle&&(t=t?.closest(ne)),t&&(e?.preventDefault(),new oe(t,ie).toggle())})})});class se{config;liveRegion=null;focusHistory=[];constructor(e={}){this.config={announcements:!0,skipLinks:!0,focusManagement:!0,keyboardNavigation:!0,reducedMotion:!0,...e},this.init()}init(){this.config.announcements&&this.createLiveRegion(),this.config.skipLinks&&this.addSkipLinks(),this.config.focusManagement&&this.initFocusManagement(),this.config.keyboardNavigation&&this.initKeyboardNavigation(),this.config.reducedMotion&&this.respectReducedMotion(),this.initErrorAnnouncements(),this.initTableAccessibility(),this.initFormAccessibility()}createLiveRegion(){this.liveRegion||(this.liveRegion=document.createElement("div"),this.liveRegion.id="live-region",this.liveRegion.className="live-region",this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("aria-atomic","true"),this.liveRegion.setAttribute("role","status"),document.body.append(this.liveRegion))}addSkipLinks(){const e=document.createElement("div");e.className="skip-links";const t=document.createElement("a");t.href="#main",t.className="skip-link",t.textContent="Skip to main content";const n=document.createElement("a");n.href="#navigation",n.className="skip-link",n.textContent="Skip to navigation",e.append(t),e.append(n),document.body.insertBefore(e,document.body.firstChild),this.ensureSkipTargets()}ensureSkipTargets(){const e=document.querySelector('#main, main, [role="main"]');e&&!e.id&&(e.id="main"),e&&!e.hasAttribute("tabindex")&&e.setAttribute("tabindex","-1");const t=document.querySelector('#navigation, nav, [role="navigation"]');t&&!t.id&&(t.id="navigation"),t&&!t.hasAttribute("tabindex")&&t.setAttribute("tabindex","-1")}initFocusManagement(){document.addEventListener("keydown",e=>{"Tab"===e.key&&this.handleTabNavigation(e),"Escape"===e.key&&this.handleEscapeKey(e)}),this.initModalFocusManagement(),this.initDropdownFocusManagement()}handleTabNavigation(e){const t=this.getFocusableElements(),n=t.indexOf(document.activeElement);e.shiftKey?n<=0&&(e.preventDefault(),t.at(-1)?.focus()):n>=t.length-1&&(e.preventDefault(),t[0]?.focus())}getFocusableElements(){const e=["a[href]","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])','[contenteditable="true"]'].join(", ");return Array.from(document.querySelectorAll(e))}handleEscapeKey(e){const t=document.querySelector(".modal.show"),n=document.querySelector(".dropdown-menu.show");if(t){const n=t.querySelector('[data-bs-dismiss="modal"]');n?.click(),e.preventDefault()}else if(n){const t=document.querySelector('[data-bs-toggle="dropdown"][aria-expanded="true"]');t?.click(),e.preventDefault()}}initKeyboardNavigation(){document.addEventListener("keydown",e=>{const t=e.target;t.closest(".nav, .navbar-nav, .dropdown-menu")&&this.handleMenuNavigation(e),"Enter"!==e.key&&" "!==e.key||!t.hasAttribute("role")||"button"!==t.getAttribute("role")||t.matches('button, input[type="button"], input[type="submit"]')||(e.preventDefault(),t.click())})}handleMenuNavigation(e){if(!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End"].includes(e.key))return;const t=e.target,n=Array.from(t.closest(".nav, .navbar-nav, .dropdown-menu")?.querySelectorAll("a, button")||[]),i=n.indexOf(t);let o;switch(e.key){case"ArrowDown":case"ArrowRight":o=i0?i-1:n.length-1;break;case"Home":o=0;break;case"End":o=n.length-1;break;default:return}e.preventDefault(),n[o]?.focus()}respectReducedMotion(){if(globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches){document.body.classList.add("reduce-motion"),document.documentElement.style.scrollBehavior="auto";const e=document.createElement("style");e.textContent="\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n ",document.head.append(e)}}initErrorAnnouncements(){new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e;t.matches(".alert-danger, .invalid-feedback, .error")&&this.announce(t.textContent||"Error occurred","assertive"),t.matches(".alert-success, .success")&&this.announce(t.textContent||"Success","polite")}})})}).observe(document.body,{childList:!0,subtree:!0})}initTableAccessibility(){document.querySelectorAll("table").forEach(e=>{if(e.hasAttribute("role")||e.setAttribute("role","table"),e.querySelectorAll("th").forEach(e=>{if(!e.hasAttribute("scope")){const t=e.closest("thead"),n=0===e.cellIndex;t?e.setAttribute("scope","col"):n&&e.setAttribute("scope","row")}}),!e.querySelector("caption")&&e.hasAttribute("title")){const t=document.createElement("caption");t.textContent=e.getAttribute("title")||"",e.insertBefore(t,e.firstChild)}})}initFormAccessibility(){document.querySelectorAll("input, select, textarea").forEach(e=>{const t=e;if(!t.labels?.length&&!t.hasAttribute("aria-label")&&!t.hasAttribute("aria-labelledby")){const e=t.getAttribute("placeholder");e&&t.setAttribute("aria-label",e)}if(t.hasAttribute("required")){const e=t.labels?.[0];if(e&&!e.querySelector(".required-indicator")){const t=document.createElement("span");t.className="required-indicator sr-only",t.textContent=" (required)",e.append(t)}}t.addEventListener("invalid",()=>{this.handleFormError(t)})})}handleFormError(e){const t=`${e.id||e.name}-error`;let n=document.getElementById(t);n||(n=document.createElement("div"),n.id=t,n.className="invalid-feedback",n.setAttribute("role","alert"),e.parentNode?.insertBefore(n,e.nextSibling)),n.textContent=e.validationMessage,e.setAttribute("aria-describedby",t),e.classList.add("is-invalid"),this.announce(`Error in ${e.labels?.[0]?.textContent||e.name}: ${e.validationMessage}`,"assertive")}initModalFocusManagement(){document.addEventListener("shown.bs.modal",e=>{const t=e.target.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');t.length>0&&t[0].focus(),this.focusHistory.push(document.activeElement)}),document.addEventListener("hidden.bs.modal",()=>{const e=this.focusHistory.pop();e&&e.focus()})}initDropdownFocusManagement(){document.addEventListener("shown.bs.dropdown",e=>{const t=e.target.querySelector(".dropdown-menu"),n=t?.querySelector("a, button");n&&n.focus()})}announce(e,t="polite"){this.liveRegion||this.createLiveRegion(),this.liveRegion&&(this.liveRegion.setAttribute("aria-live",t),this.liveRegion.textContent=e,setTimeout(()=>{this.liveRegion&&(this.liveRegion.textContent="")},1e3))}focusElement(e){const t=document.querySelector(e);t&&(t.focus(),t.scrollIntoView({behavior:"smooth",block:"center"}))}trapFocus(e){const t=e.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),n=Array.from(t),i=n[0],o=n.at(-1);e.addEventListener("keydown",e=>{"Tab"===e.key&&(e.shiftKey?document.activeElement===i&&(o?.focus(),e.preventDefault()):document.activeElement===o&&(i.focus(),e.preventDefault()))})}addLandmarks(){if(!document.querySelector("main")){const e=document.querySelector(".app-main");e&&(e.setAttribute("role","main"),e.id="main")}document.querySelectorAll(".navbar-nav, .nav").forEach((e,t)=>{e.hasAttribute("role")||e.setAttribute("role","navigation"),e.hasAttribute("aria-label")||e.setAttribute("aria-label",`Navigation ${t+1}`)});const e=document.querySelector('form[role="search"], .navbar-search');e&&!e.hasAttribute("role")&&e.setAttribute("role","search")}}const ae=e=>new se(e);n(()=>{new a(document.body).holdTransition(),ae({announcements:!0,skipLinks:!0,focusManagement:!0,keyboardNavigation:!0,reducedMotion:!0}).addLandmarks(),setTimeout(()=>{document.body.classList.add("app-loaded")},400)}),e.CardWidget=k,e.DirectChat=H,e.FullScreen=V,e.Layout=a,e.PushMenu=oe,e.Treeview=F,e.initAccessibility=ae}); +/*! + * AdminLTE v4.0.0-rc5 (https://adminlte.io) + * Copyright 2014-2025 Colorlib + * Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE) + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).adminlte={})}(this,function(e){"use strict";const t=[],n=e=>{"loading"===document.readyState?(t.length||document.addEventListener("DOMContentLoaded",()=>{for(const e of t)e()}),t.push(e)):e()},i=(e,t=500)=>{e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.boxSizing="border-box",e.style.height=`${e.offsetHeight}px`,e.style.overflow="hidden",globalThis.setTimeout(()=>{e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0"},1),globalThis.setTimeout(()=>{e.style.display="none",e.style.removeProperty("height"),e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")},t)},o=(e,t=500)=>{e.style.removeProperty("display");let{display:n}=globalThis.getComputedStyle(e);"none"===n&&(n="block"),e.style.display=n;const i=e.offsetHeight;e.style.overflow="hidden",e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0",globalThis.setTimeout(()=>{e.style.boxSizing="border-box",e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.height=`${i}px`,e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom")},1),globalThis.setTimeout(()=>{e.style.removeProperty("height"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")},t)},s="hold-transition";class a{_element;constructor(e){this._element=e}holdTransition(){let e;window.addEventListener("resize",()=>{document.body.classList.add(s),clearTimeout(e),e=setTimeout(()=>{document.body.classList.remove(s)},400)})}}n(()=>{new a(document.body).holdTransition(),setTimeout(()=>{document.body.classList.add("app-loaded")},400)});const r=".lte.card-widget",l=`collapsed${r}`,c=`expanded${r}`,d=`remove${r}`,u=`maximized${r}`,m=`minimized${r}`,h="card",p="collapsed-card",g="collapsing-card",y="expanding-card",v="was-collapsed",b="maximized-card",f='[data-lte-toggle="card-remove"]',E='[data-lte-toggle="card-collapse"]',_='[data-lte-toggle="card-maximize"]',S=`.${h}`,L=".card-body",w=".card-footer",A={animationSpeed:500,collapseTrigger:E,removeTrigger:f,maximizeTrigger:_};class k{_element;_parent;_clone;_config;constructor(e,t){this._element=e,this._parent=e.closest(S),e.classList.contains(h)&&(this._parent=e),this._config={...A,...t}}collapse(){const e=new Event(l);if(this._parent){this._parent.classList.add(g);const e=this._parent?.querySelectorAll(`${L}, ${w}`);e.forEach(e=>{e instanceof HTMLElement&&i(e,this._config.animationSpeed)}),setTimeout(()=>{this._parent&&(this._parent.classList.add(p),this._parent.classList.remove(g))},this._config.animationSpeed)}this._element?.dispatchEvent(e)}expand(){const e=new Event(c);if(this._parent){this._parent.classList.add(y);const e=this._parent?.querySelectorAll(`${L}, ${w}`);e.forEach(e=>{e instanceof HTMLElement&&o(e,this._config.animationSpeed)}),setTimeout(()=>{this._parent&&this._parent.classList.remove(p,y)},this._config.animationSpeed)}this._element?.dispatchEvent(e)}remove(){const e=new Event(d);this._parent&&i(this._parent,this._config.animationSpeed),this._element?.dispatchEvent(e)}toggle(){this._parent?.classList.contains(p)?this.expand():this.collapse()}maximize(){const e=new Event(u);this._parent&&(this._parent.style.height=`${this._parent.offsetHeight}px`,this._parent.style.width=`${this._parent.offsetWidth}px`,this._parent.style.transition="all .15s",setTimeout(()=>{const e=document.querySelector("html");e&&e.classList.add(b),this._parent&&(this._parent.classList.add(b),this._parent.classList.contains(p)&&this._parent.classList.add(v))},150)),this._element?.dispatchEvent(e)}minimize(){const e=new Event(m);this._parent&&(this._parent.style.height="auto",this._parent.style.width="auto",this._parent.style.transition="all .15s",setTimeout(()=>{const e=document.querySelector("html");e&&e.classList.remove(b),this._parent&&(this._parent.classList.remove(b),this._parent?.classList.contains(v)&&this._parent.classList.remove(v))},10)),this._element?.dispatchEvent(e)}toggleMaximize(){this._parent?.classList.contains(b)?this.minimize():this.maximize()}}n(()=>{document.querySelectorAll(E).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new k(t,A).toggle()})}),document.querySelectorAll(f).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new k(t,A).remove()})}),document.querySelectorAll(_).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new k(t,A).toggleMaximize()})})});const x=".lte.treeview",q=`expanded${x}`,T=`collapsed${x}`,$=`load${x}`,M="menu-open",D=".nav-item",N=".nav-treeview",P={animationSpeed:300,accordion:!0};class F{_element;_config;constructor(e,t){this._element=e,this._config={...P,...t}}open(){const e=new Event(q);if(this._config.accordion){const e=this._element.parentElement?.querySelectorAll(`${D}.${M}`);e?.forEach(e=>{if(e!==this._element.parentElement){e.classList.remove(M);const t=e?.querySelector(N);t&&i(t,this._config.animationSpeed)}})}this._element.classList.add(M);const t=this._element?.querySelector(N);t&&o(t,this._config.animationSpeed),this._element.dispatchEvent(e)}close(){const e=new Event(T);this._element.classList.remove(M);const t=this._element?.querySelector(N);t&&i(t,this._config.animationSpeed),this._element.dispatchEvent(e)}toggle(){this._element.classList.contains(M)?this.close():this.open()}}n(()=>{document.querySelectorAll(`${D}.${M}`).forEach(e=>{const t=e.querySelector(N);if(t){o(t,0);const n=new Event($);e.dispatchEvent(n)}}),document.querySelectorAll('[data-lte-toggle="treeview"]').forEach(e=>{e.addEventListener("click",e=>{const t=e.target,n=t.closest(D),i=t.closest(".nav-link"),o=e.currentTarget;if("#"!==t?.getAttribute("href")&&"#"!==i?.getAttribute("href")||e.preventDefault(),n){const e=o.dataset.accordion,t=o.dataset.animationSpeed,i={accordion:void 0===e?P.accordion:"true"===e,animationSpeed:void 0===t?P.animationSpeed:Number(t)};new F(n,i).toggle()}})})});const R=".lte.direct-chat",C=`expanded${R}`,z=`collapsed${R}`,B="direct-chat-contacts-open";class H{_element;constructor(e){this._element=e}toggle(){if(this._element.classList.contains(B)){const e=new Event(z);this._element.classList.remove(B),this._element.dispatchEvent(e)}else{const e=new Event(C);this._element.classList.add(B),this._element.dispatchEvent(e)}}}n(()=>{document.querySelectorAll('[data-lte-toggle="chat-pane"]').forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target.closest(".direct-chat");t&&new H(t).toggle()})})});const K=".lte.fullscreen",O=`maximized${K}`,W=`minimized${K}`,I='[data-lte-toggle="fullscreen"]',j='[data-lte-icon="maximize"]',U='[data-lte-icon="minimize"]';class V{_element;_config;constructor(e,t){this._element=e,this._config=t}inFullScreen(){const e=new Event(O),t=document.querySelector(j),n=document.querySelector(U);document.documentElement.requestFullscreen(),t&&(t.style.display="none"),n&&(n.style.display="block"),this._element.dispatchEvent(e)}outFullscreen(){const e=new Event(W),t=document.querySelector(j),n=document.querySelector(U);document.exitFullscreen(),t&&(t.style.display="block"),n&&(n.style.display="none"),this._element.dispatchEvent(e)}toggleFullScreen(){document.fullscreenEnabled&&(document.fullscreenElement?this.outFullscreen():this.inFullScreen())}}n(()=>{document.querySelectorAll(I).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target.closest(I);t&&new V(t,void 0).toggleFullScreen()})})});const G=".lte.push-menu",J=`open${G}`,Q=`collapse${G}`,X="sidebar-mini",Y="sidebar-collapse",Z="sidebar-open",ee="sidebar-expand",te=`[class*="${ee}"]`,ne='[data-lte-toggle="sidebar"]',ie={sidebarBreakpoint:992};class oe{_element;_config;constructor(e,t){this._element=e,this._config={...ie,...t}}menusClose(){document.querySelectorAll(".nav-treeview").forEach(e=>{e.style.removeProperty("display"),e.style.removeProperty("height")});const e=document.querySelector(".sidebar-menu"),t=e?.querySelectorAll(".nav-item");t&&t.forEach(e=>{e.classList.remove("menu-open")})}expand(){const e=new Event(J);document.body.classList.remove(Y),document.body.classList.add(Z),this._element.dispatchEvent(e)}collapse(){const e=new Event(Q);document.body.classList.remove(Z),document.body.classList.add(Y),this._element.dispatchEvent(e)}addSidebarBreakPoint(){const e=document.querySelector(te)?.classList??[],t=Array.from(e).find(e=>e.startsWith(ee))??"",n=document.getElementsByClassName(t)[0],i=globalThis.getComputedStyle(n,"::before").getPropertyValue("content");this._config={...this._config,sidebarBreakpoint:Number(i.replace(/[^\d.-]/g,""))};const o=document.body.classList.contains(Z);window.innerWidth<=this._config.sidebarBreakpoint?o||this.collapse():(document.body.classList.contains(X)||this.expand(),document.body.classList.contains(X)&&document.body.classList.contains(Y)&&this.collapse())}toggle(){document.body.classList.contains(Y)?this.expand():this.collapse()}init(){this.addSidebarBreakPoint()}}n(()=>{const e=document?.querySelector(".app-sidebar");if(e){const t=new oe(e,ie);t.init(),window.addEventListener("resize",()=>{t.init()})}const t=document.createElement("div");t.className="sidebar-overlay",document.querySelector(".app-wrapper")?.append(t);let n=!1;t.addEventListener("touchstart",()=>{n=!1},{passive:!0}),t.addEventListener("touchmove",()=>{n=!0},{passive:!0}),t.addEventListener("touchend",e=>{if(!n){e.preventDefault();const t=e.currentTarget;new oe(t,ie).collapse()}n=!1},{passive:!1}),t.addEventListener("click",e=>{e.preventDefault();const t=e.currentTarget;new oe(t,ie).collapse()}),document.querySelectorAll(ne).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();let t=e.currentTarget;"sidebar"!==t?.dataset.lteToggle&&(t=t?.closest(ne)),t&&(e?.preventDefault(),new oe(t,ie).toggle())})})});class se{config;liveRegion=null;focusHistory=[];constructor(e={}){this.config={announcements:!0,skipLinks:!0,focusManagement:!0,keyboardNavigation:!0,reducedMotion:!0,...e},this.init()}init(){this.config.announcements&&this.createLiveRegion(),this.config.skipLinks&&this.addSkipLinks(),this.config.focusManagement&&this.initFocusManagement(),this.config.keyboardNavigation&&this.initKeyboardNavigation(),this.config.reducedMotion&&this.respectReducedMotion(),this.initErrorAnnouncements(),this.initTableAccessibility(),this.initFormAccessibility()}createLiveRegion(){this.liveRegion||(this.liveRegion=document.createElement("div"),this.liveRegion.id="live-region",this.liveRegion.className="live-region",this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("aria-atomic","true"),this.liveRegion.setAttribute("role","status"),document.body.append(this.liveRegion))}addSkipLinks(){const e=document.createElement("div");e.className="skip-links";const t=document.createElement("a");t.href="#main",t.className="skip-link",t.textContent="Skip to main content";const n=document.createElement("a");n.href="#navigation",n.className="skip-link",n.textContent="Skip to navigation",e.append(t),e.append(n),document.body.insertBefore(e,document.body.firstChild),this.ensureSkipTargets()}ensureSkipTargets(){const e=document.querySelector('#main, main, [role="main"]');e&&!e.id&&(e.id="main"),e&&!e.hasAttribute("tabindex")&&e.setAttribute("tabindex","-1");const t=document.querySelector('#navigation, nav, [role="navigation"]');t&&!t.id&&(t.id="navigation"),t&&!t.hasAttribute("tabindex")&&t.setAttribute("tabindex","-1")}initFocusManagement(){document.addEventListener("keydown",e=>{"Tab"===e.key&&this.handleTabNavigation(e),"Escape"===e.key&&this.handleEscapeKey(e)}),this.initModalFocusManagement(),this.initDropdownFocusManagement()}handleTabNavigation(e){const t=this.getFocusableElements(),n=t.indexOf(document.activeElement);e.shiftKey?n<=0&&(e.preventDefault(),t.at(-1)?.focus()):n>=t.length-1&&(e.preventDefault(),t[0]?.focus())}getFocusableElements(){const e=["a[href]","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])','[contenteditable="true"]'].join(", ");return Array.from(document.querySelectorAll(e))}handleEscapeKey(e){const t=document.querySelector(".modal.show"),n=document.querySelector(".dropdown-menu.show");if(t){const n=t.querySelector('[data-bs-dismiss="modal"]');n?.click(),e.preventDefault()}else if(n){const t=document.querySelector('[data-bs-toggle="dropdown"][aria-expanded="true"]');t?.click(),e.preventDefault()}}initKeyboardNavigation(){document.addEventListener("keydown",e=>{const t=e.target;t.closest(".nav, .navbar-nav, .dropdown-menu")&&this.handleMenuNavigation(e),"Enter"!==e.key&&" "!==e.key||!t.hasAttribute("role")||"button"!==t.getAttribute("role")||t.matches('button, input[type="button"], input[type="submit"]')||(e.preventDefault(),t.click())})}handleMenuNavigation(e){if(!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End"].includes(e.key))return;const t=e.target,n=Array.from(t.closest(".nav, .navbar-nav, .dropdown-menu")?.querySelectorAll("a, button")||[]),i=n.indexOf(t);let o;switch(e.key){case"ArrowDown":case"ArrowRight":o=i0?i-1:n.length-1;break;case"Home":o=0;break;case"End":o=n.length-1;break;default:return}e.preventDefault(),n[o]?.focus()}respectReducedMotion(){if(globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches){document.body.classList.add("reduce-motion"),document.documentElement.style.scrollBehavior="auto";const e=document.createElement("style");e.textContent="\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n ",document.head.append(e)}}initErrorAnnouncements(){new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e;t.matches(".alert-danger, .invalid-feedback, .error")&&this.announce(t.textContent||"Error occurred","assertive"),t.matches(".alert-success, .success")&&this.announce(t.textContent||"Success","polite")}})})}).observe(document.body,{childList:!0,subtree:!0})}initTableAccessibility(){document.querySelectorAll("table").forEach(e=>{if(e.hasAttribute("role")||e.setAttribute("role","table"),e.querySelectorAll("th").forEach(e=>{if(!e.hasAttribute("scope")){const t=e.closest("thead"),n=0===e.cellIndex;t?e.setAttribute("scope","col"):n&&e.setAttribute("scope","row")}}),!e.querySelector("caption")&&e.hasAttribute("title")){const t=document.createElement("caption");t.textContent=e.getAttribute("title")||"",e.insertBefore(t,e.firstChild)}})}initFormAccessibility(){document.querySelectorAll("input, select, textarea").forEach(e=>{const t=e;if(!t.labels?.length&&!t.hasAttribute("aria-label")&&!t.hasAttribute("aria-labelledby")){const e=t.getAttribute("placeholder");e&&t.setAttribute("aria-label",e)}if(t.hasAttribute("required")){const e=t.labels?.[0];if(e&&!e.querySelector(".required-indicator")){const t=document.createElement("span");t.className="required-indicator sr-only",t.textContent=" (required)",e.append(t)}}t.addEventListener("invalid",()=>{this.handleFormError(t)})})}handleFormError(e){const t=`${e.id||e.name}-error`;let n=document.getElementById(t);n||(n=document.createElement("div"),n.id=t,n.className="invalid-feedback",n.setAttribute("role","alert"),e.parentNode?.insertBefore(n,e.nextSibling)),n.textContent=e.validationMessage,e.setAttribute("aria-describedby",t),e.classList.add("is-invalid"),this.announce(`Error in ${e.labels?.[0]?.textContent||e.name}: ${e.validationMessage}`,"assertive")}initModalFocusManagement(){document.addEventListener("shown.bs.modal",e=>{const t=e.target.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');t.length>0&&t[0].focus(),this.focusHistory.push(document.activeElement)}),document.addEventListener("hidden.bs.modal",()=>{const e=this.focusHistory.pop();e&&e.focus()})}initDropdownFocusManagement(){document.addEventListener("shown.bs.dropdown",e=>{const t=e.target.querySelector(".dropdown-menu"),n=t?.querySelector("a, button");n&&n.focus()})}announce(e,t="polite"){this.liveRegion||this.createLiveRegion(),this.liveRegion&&(this.liveRegion.setAttribute("aria-live",t),this.liveRegion.textContent=e,setTimeout(()=>{this.liveRegion&&(this.liveRegion.textContent="")},1e3))}focusElement(e){const t=document.querySelector(e);t&&(t.focus(),t.scrollIntoView({behavior:"smooth",block:"center"}))}trapFocus(e){const t=e.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),n=Array.from(t),i=n[0],o=n.at(-1);e.addEventListener("keydown",e=>{"Tab"===e.key&&(e.shiftKey?document.activeElement===i&&(o?.focus(),e.preventDefault()):document.activeElement===o&&(i.focus(),e.preventDefault()))})}addLandmarks(){if(!document.querySelector("main")){const e=document.querySelector(".app-main");e&&(e.setAttribute("role","main"),e.id="main")}document.querySelectorAll(".navbar-nav, .nav").forEach((e,t)=>{e.hasAttribute("role")||e.setAttribute("role","navigation"),e.hasAttribute("aria-label")||e.setAttribute("aria-label",`Navigation ${t+1}`)});const e=document.querySelector('form[role="search"], .navbar-search');e&&!e.hasAttribute("role")&&e.setAttribute("role","search")}}const ae=e=>new se(e);n(()=>{new a(document.body).holdTransition(),ae({announcements:!0,skipLinks:!0,focusManagement:!0,keyboardNavigation:!0,reducedMotion:!0}).addLandmarks(),setTimeout(()=>{document.body.classList.add("app-loaded")},400)}),e.CardWidget=k,e.DirectChat=H,e.FullScreen=V,e.Layout=a,e.PushMenu=oe,e.Treeview=F,e.initAccessibility=ae}); //# sourceMappingURL=adminlte.min.js.map \ No newline at end of file diff --git a/js/auth.js b/js/auth.js index 1ce9c34..e405002 100644 --- a/js/auth.js +++ b/js/auth.js @@ -1,149 +1,149 @@ - async function checkRegistration() { - try { - - if (!window.fetch || !navigator.credentials || !navigator.credentials.create) { - throw new Error('Browser not supported.'); - } - - // get check args - let rep = await window.fetch('authServer.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'}); - const getArgs = await rep.json(); - - // error handling - if (getArgs.success === false) { - throw new Error(getArgs.msg); - } - - // replace binary base64 data with ArrayBuffer. a other way to do this - // is the reviver function of JSON.parse() - recursiveBase64StrToArrayBuffer(getArgs); - - // check credentials with hardware - const cred = await navigator.credentials.get(getArgs); - - // create object for transmission to server - const authenticatorAttestationResponse = { - id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, - clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, - authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, - signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null, - userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null - }; - - // send to server - rep = await window.fetch('authServer.php?fn=processGet' + getGetParams(), { - method:'POST', - body: JSON.stringify(authenticatorAttestationResponse), - cache:'no-cache' - }); - const authenticatorAttestationServerResponse = await rep.json(); - - // check server response - if (authenticatorAttestationServerResponse.success) { - window.location.reload(); - /// window.alert(authenticatorAttestationServerResponse.msg || 'login success'); - - } else { - throw new Error(authenticatorAttestationServerResponse.msg); - } - - } catch (err) { - //reloadServerPreview(); - window.alert(err.message || 'unknown error occured'); - } - } - - function queryFidoMetaDataService() { - window.fetch('authServer.php?fn=queryFidoMetaDataService' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) { - return response.json(); - - }).then(function(json) { - if (json.success) { - window.alert(json.msg); - } else { - throw new Error(json.msg); - } - }).catch(function(err) { - window.alert(err.message || 'unknown error occured'); - }); - } - - /** - * convert RFC 1342-like base64 strings to array buffer - * @param {mixed} obj - * @returns {undefined} - */ - function recursiveBase64StrToArrayBuffer(obj) { - let prefix = '=?BINARY?B?'; - let suffix = '?='; - if (typeof obj === 'object') { - for (let key in obj) { - if (typeof obj[key] === 'string') { - let str = obj[key]; - if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) { - str = str.substring(prefix.length, str.length - suffix.length); - - let binary_string = window.atob(str); - let len = binary_string.length; - let bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binary_string.charCodeAt(i); - } - obj[key] = bytes.buffer; - } - } else { - recursiveBase64StrToArrayBuffer(obj[key]); - } - } - } - } - - /** - * Convert a ArrayBuffer to Base64 - * @param {ArrayBuffer} buffer - * @returns {String} - */ - function arrayBufferToBase64(buffer) { - let binary = ''; - let bytes = new Uint8Array(buffer); - let len = bytes.byteLength; - for (let i = 0; i < len; i++) { - binary += String.fromCharCode( bytes[ i ] ); - } - return window.btoa(binary); - } - - /** - * Get URL parameter - * @returns {String} - */ - function getGetParams() { - let url = ''; - - url += '&apple=1'; - url += '&yubico=1'; - url += '&solo=1'; - url += '&hypersecu=1'; - url += '&google=1'; - url += 'µsoft=1'; - url += '&mds=1'; - - url += '&requireResidentKey=0'; - - url += '&type_usb=1'; - url += '&type_nfc=1'; - url += '&type_ble=1'; - url += '&type_int=1'; - url += '&type_hybrid=1'; - - url += '&fmt_android-key=1'; - url += '&fmt_android-safetynet=1'; - url += '&fmt_apple=1'; - url += '&fmt_fido-u2f=1'; - url += '&fmt_none=0' ; - url += '&fmt_packed=1'; - url += '&fmt_tpm=1'; - url += '&userVerification=discouraged'; - - return url; + async function checkRegistration() { + try { + + if (!window.fetch || !navigator.credentials || !navigator.credentials.create) { + throw new Error('Browser not supported.'); + } + + // get check args + let rep = await window.fetch('authServer.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'}); + const getArgs = await rep.json(); + + // error handling + if (getArgs.success === false) { + throw new Error(getArgs.msg); + } + + // replace binary base64 data with ArrayBuffer. a other way to do this + // is the reviver function of JSON.parse() + recursiveBase64StrToArrayBuffer(getArgs); + + // check credentials with hardware + const cred = await navigator.credentials.get(getArgs); + + // create object for transmission to server + const authenticatorAttestationResponse = { + id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, + clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, + authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, + signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null, + userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null + }; + + // send to server + rep = await window.fetch('authServer.php?fn=processGet' + getGetParams(), { + method:'POST', + body: JSON.stringify(authenticatorAttestationResponse), + cache:'no-cache' + }); + const authenticatorAttestationServerResponse = await rep.json(); + + // check server response + if (authenticatorAttestationServerResponse.success) { + window.location.reload(); + /// window.alert(authenticatorAttestationServerResponse.msg || 'login success'); + + } else { + throw new Error(authenticatorAttestationServerResponse.msg); + } + + } catch (err) { + //reloadServerPreview(); + window.alert(err.message || 'unknown error occured'); + } + } + + function queryFidoMetaDataService() { + window.fetch('authServer.php?fn=queryFidoMetaDataService' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) { + return response.json(); + + }).then(function(json) { + if (json.success) { + window.alert(json.msg); + } else { + throw new Error(json.msg); + } + }).catch(function(err) { + window.alert(err.message || 'unknown error occured'); + }); + } + + /** + * convert RFC 1342-like base64 strings to array buffer + * @param {mixed} obj + * @returns {undefined} + */ + function recursiveBase64StrToArrayBuffer(obj) { + let prefix = '=?BINARY?B?'; + let suffix = '?='; + if (typeof obj === 'object') { + for (let key in obj) { + if (typeof obj[key] === 'string') { + let str = obj[key]; + if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) { + str = str.substring(prefix.length, str.length - suffix.length); + + let binary_string = window.atob(str); + let len = binary_string.length; + let bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + obj[key] = bytes.buffer; + } + } else { + recursiveBase64StrToArrayBuffer(obj[key]); + } + } + } + } + + /** + * Convert a ArrayBuffer to Base64 + * @param {ArrayBuffer} buffer + * @returns {String} + */ + function arrayBufferToBase64(buffer) { + let binary = ''; + let bytes = new Uint8Array(buffer); + let len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode( bytes[ i ] ); + } + return window.btoa(binary); + } + + /** + * Get URL parameter + * @returns {String} + */ + function getGetParams() { + let url = ''; + + url += '&apple=1'; + url += '&yubico=1'; + url += '&solo=1'; + url += '&hypersecu=1'; + url += '&google=1'; + url += 'µsoft=1'; + url += '&mds=1'; + + url += '&requireResidentKey=0'; + + url += '&type_usb=1'; + url += '&type_nfc=1'; + url += '&type_ble=1'; + url += '&type_int=1'; + url += '&type_hybrid=1'; + + url += '&fmt_android-key=1'; + url += '&fmt_android-safetynet=1'; + url += '&fmt_apple=1'; + url += '&fmt_fido-u2f=1'; + url += '&fmt_none=0' ; + url += '&fmt_packed=1'; + url += '&fmt_tpm=1'; + url += '&userVerification=discouraged'; + + return url; } \ No newline at end of file diff --git a/js/bootstrap.min.js b/js/bootstrap.min.js index 54cc3c7..20da8f7 100644 --- a/js/bootstrap.min.js +++ b/js/bootstrap.min.js @@ -1,7 +1,7 @@ -/*! - * Bootstrap v5.3.7 (https://getbootstrap.com/) - * Copyright 2011-2025 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){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=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?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=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 L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&P(t,e,i,r.callable,r.delegationSelector)}function M(t){return t=t.replace(E,""),$[t]||t}const j={on(t,e,i,s){N(t,e,i,s,!1)},one(t,e,i,s){N(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=D(e,i,s),a=r!==e,l=O(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))x(t,l,i,e.slice(1));for(const[i,s]of Object.entries(c)){const n=i.replace(C,"");a&&!e.includes(n)||P(t,l,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;P(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=f();let n=null,o=!0,r=!0,a=!1;e!==M(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());const l=F(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function F(t,e={}){for(const[i,s]of Object.entries(e))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}function z(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function H(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const B={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${H(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${H(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=z(t.dataset[s])}return e},getDataAttribute:(t,e)=>z(t.getAttribute(`data-bs-${H(e)}`))};class q{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=l(e)?B.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...l(e)?B.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[s,n]of Object.entries(e)){const e=t[s],o=l(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(n).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${s}" provided type "${o}" but expected type "${n}".`)}var i}}class W extends q{constructor(t,e){super(),(t=c(t))&&(this._element=t,this._config=this._getConfig(e),n.set(this._element,this.constructor.DATA_KEY,this))}dispose(){n.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){y(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return n.get(c(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.3.7"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const R=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>r(t))).join(","):null},K={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 i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!d(t)&&h(t)))},getSelectorFromElement(t){const e=R(t);return e&&K.findOne(e)?e:null},getElementFromSelector(t){const e=R(t);return e?K.findOne(e):null},getMultipleElementsFromSelector(t){const e=R(t);return e?K.find(e):[]}},V=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),d(this))return;const n=K.getElementFromSelector(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()}))},Q=".bs.alert",X=`close${Q}`,Y=`closed${Q}`;class U extends W{static get NAME(){return"alert"}close(){if(j.trigger(this._element,X).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,Y),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=U.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}V(U,"close"),b(U);const G='[data-bs-toggle="button"]';class J extends W{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=J.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}j.on(document,"click.bs.button.data-api",G,(t=>{t.preventDefault();const e=t.target.closest(G);J.getOrCreateInstance(e).toggle()})),b(J);const Z=".bs.swipe",tt=`touchstart${Z}`,et=`touchmove${Z}`,it=`touchend${Z}`,st=`pointerdown${Z}`,nt=`pointerup${Z}`,ot={endCallback:null,leftCallback:null,rightCallback:null},rt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class at extends q{constructor(t,e){super(),this._element=t,t&&at.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return ot}static get DefaultType(){return rt}static get NAME(){return"swipe"}dispose(){j.off(this._element,Z)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),v(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&v(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(j.on(this._element,st,(t=>this._start(t))),j.on(this._element,nt,(t=>this._end(t))),this._element.classList.add("pointer-event")):(j.on(this._element,tt,(t=>this._start(t))),j.on(this._element,et,(t=>this._move(t))),j.on(this._element,it,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const lt=".bs.carousel",ct=".data-api",ht="ArrowLeft",dt="ArrowRight",ut="next",_t="prev",gt="left",ft="right",mt=`slide${lt}`,pt=`slid${lt}`,bt=`keydown${lt}`,vt=`mouseenter${lt}`,yt=`mouseleave${lt}`,wt=`dragstart${lt}`,At=`load${lt}${ct}`,Et=`click${lt}${ct}`,Ct="carousel",Tt="active",kt=".active",$t=".carousel-item",St=kt+$t,Lt={[ht]:ft,[dt]:gt},Ot={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},It={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Dt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=K.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Ct&&this.cycle()}static get Default(){return Ot}static get DefaultType(){return It}static get NAME(){return"carousel"}next(){this._slide(ut)}nextWhenVisible(){!document.hidden&&h(this._element)&&this.next()}prev(){this._slide(_t)}pause(){this._isSliding&&a(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?j.one(this._element,pt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,pt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?ut:_t;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&j.on(this._element,bt,(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,vt,(()=>this.pause())),j.on(this._element,yt,(()=>this._maybeEnableCycle()))),this._config.touch&&at.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of K.find(".carousel-item img",this._element))j.on(t,wt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(gt)),rightCallback:()=>this._slide(this._directionToOrder(ft)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new at(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Lt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=K.findOne(kt,this._indicatorsElement);e.classList.remove(Tt),e.removeAttribute("aria-current");const i=K.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Tt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===ut,n=e||w(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>j.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(mt).defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),g(n),i.classList.add(l),n.classList.add(l),this._queueCallback((()=>{n.classList.remove(l,c),n.classList.add(Tt),i.classList.remove(Tt,c,l),this._isSliding=!1,r(pt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return K.findOne(St,this._element)}_getItems(){return K.find($t,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===gt?_t:ut:t===gt?ut:_t}_orderToDirection(t){return p()?t===_t?gt:ft:t===_t?ft:gt}static jQueryInterface(t){return this.each((function(){const e=Dt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}j.on(document,Et,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=K.getElementFromSelector(this);if(!e||!e.classList.contains(Ct))return;t.preventDefault();const i=Dt.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===B.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),j.on(window,At,(()=>{const t=K.find('[data-bs-ride="carousel"]');for(const e of t)Dt.getOrCreateInstance(e)})),b(Dt);const Nt=".bs.collapse",Pt=`show${Nt}`,xt=`shown${Nt}`,Mt=`hide${Nt}`,jt=`hidden${Nt}`,Ft=`click${Nt}.data-api`,zt="show",Ht="collapse",Bt="collapsing",qt=`:scope .${Ht} .${Ht}`,Wt='[data-bs-toggle="collapse"]',Rt={parent:null,toggle:!0},Kt={parent:"(null|element)",toggle:"boolean"};class Vt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=K.find(Wt);for(const t of i){const e=K.getSelectorFromElement(t),i=K.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Rt}static get DefaultType(){return Kt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Vt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(j.trigger(this._element,Pt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Ht),this._element.classList.add(Bt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht,zt),this._element.style[e]="",j.trigger(this._element,xt)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,Mt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,g(this._element),this._element.classList.add(Bt),this._element.classList.remove(Ht,zt);for(const t of this._triggerArray){const e=K.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht),j.trigger(this._element,jt)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(zt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=c(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Wt);for(const e of t){const t=K.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=K.find(qt,this._config.parent);return K.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Vt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,Ft,Wt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of K.getMultipleElementsFromSelector(this))Vt.getOrCreateInstance(t,{toggle:!1}).toggle()})),b(Vt);const Qt="dropdown",Xt=".bs.dropdown",Yt=".data-api",Ut="ArrowUp",Gt="ArrowDown",Jt=`hide${Xt}`,Zt=`hidden${Xt}`,te=`show${Xt}`,ee=`shown${Xt}`,ie=`click${Xt}${Yt}`,se=`keydown${Xt}${Yt}`,ne=`keyup${Xt}${Yt}`,oe="show",re='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ae=`${re}.${oe}`,le=".dropdown-menu",ce=p()?"top-end":"top-start",he=p()?"top-start":"top-end",de=p()?"bottom-end":"bottom-start",ue=p()?"bottom-start":"bottom-end",_e=p()?"left-start":"right-start",ge=p()?"right-start":"left-start",fe={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},me={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class pe extends W{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=K.next(this._element,le)[0]||K.prev(this._element,le)[0]||K.findOne(le,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return fe}static get DefaultType(){return me}static get NAME(){return Qt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(d(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!j.trigger(this._element,te,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(oe),this._element.classList.add(oe),j.trigger(this._element,ee,t)}}hide(){if(d(this._element)||!this._isShown())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()}_completeHide(t){if(!j.trigger(this._element,Jt,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._popper&&this._popper.destroy(),this._menu.classList.remove(oe),this._element.classList.remove(oe),this._element.setAttribute("aria-expanded","false"),B.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,Zt,t),this._element.focus()}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!l(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Qt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let t=this._element;"parent"===this._config.reference?t=this._parent:l(this._config.reference)?t=c(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(oe)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return _e;if(t.classList.contains("dropstart"))return ge;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?he:ce:e?ue:de}_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(this._inNavbar||"static"===this._config.display)&&(B.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...v(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=K.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>h(t)));i.length&&w(i,e,t===Gt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=pe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=K.find(ae);for(const i of e){const e=pe.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[Ut,Gt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(re)?this:K.prev(this,re)[0]||K.next(this,re)[0]||K.findOne(re,t.delegateTarget.parentNode),o=pe.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}j.on(document,se,re,pe.dataApiKeydownHandler),j.on(document,se,le,pe.dataApiKeydownHandler),j.on(document,ie,pe.clearMenus),j.on(document,ne,pe.clearMenus),j.on(document,ie,re,(function(t){t.preventDefault(),pe.getOrCreateInstance(this).toggle()})),b(pe);const be="backdrop",ve="show",ye=`mousedown.bs.${be}`,we={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ae={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ee extends q{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return we}static get DefaultType(){return Ae}static get NAME(){return be}show(t){if(!this._config.isVisible)return void v(t);this._append();const e=this._getElement();this._config.isAnimated&&g(e),e.classList.add(ve),this._emulateAnimation((()=>{v(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(ve),this._emulateAnimation((()=>{this.dispose(),v(t)}))):v(t)}dispose(){this._isAppended&&(j.off(this._element,ye),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=c(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),j.on(t,ye,(()=>{v(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){y(t,this._getElement(),this._config.isAnimated)}}const Ce=".bs.focustrap",Te=`focusin${Ce}`,ke=`keydown.tab${Ce}`,$e="backward",Se={autofocus:!0,trapElement:null},Le={autofocus:"boolean",trapElement:"element"};class Oe extends q{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Se}static get DefaultType(){return Le}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),j.off(document,Ce),j.on(document,Te,(t=>this._handleFocusin(t))),j.on(document,ke,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,Ce))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=K.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===$e?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?$e:"forward")}}const Ie=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",De=".sticky-top",Ne="padding-right",Pe="margin-right";class xe{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,Ne,(e=>e+t)),this._setElementAttributes(Ie,Ne,(e=>e+t)),this._setElementAttributes(De,Pe,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Ne),this._resetElementAttributes(Ie,Ne),this._resetElementAttributes(De,Pe)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&B.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=B.getDataAttribute(t,e);null!==i?(B.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(l(t))e(t);else for(const i of K.find(t,this._element))e(i)}}const Me=".bs.modal",je=`hide${Me}`,Fe=`hidePrevented${Me}`,ze=`hidden${Me}`,He=`show${Me}`,Be=`shown${Me}`,qe=`resize${Me}`,We=`click.dismiss${Me}`,Re=`mousedown.dismiss${Me}`,Ke=`keydown.dismiss${Me}`,Ve=`click${Me}.data-api`,Qe="modal-open",Xe="show",Ye="modal-static",Ue={backdrop:!0,focus:!0,keyboard:!0},Ge={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Je extends W{constructor(t,e){super(t,e),this._dialog=K.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new xe,this._addEventListeners()}static get Default(){return Ue}static get DefaultType(){return Ge}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,He,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Qe),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(j.trigger(this._element,je).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Xe),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){j.off(window,Me),j.off(this._dialog,Me),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ee({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(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;const e=K.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),g(this._element),this._element.classList.add(Xe),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,Be,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){j.on(this._element,Ke,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),j.on(window,qe,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),j.on(this._element,Re,(t=>{j.one(this._element,We,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_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(Qe),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,ze)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,Fe).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Ye)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Ye),this._queueCallback((()=>{this._element.classList.remove(Ye),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Je.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,Ve,'[data-bs-toggle="modal"]',(function(t){const e=K.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,He,(t=>{t.defaultPrevented||j.one(e,ze,(()=>{h(this)&&this.focus()}))}));const i=K.findOne(".modal.show");i&&Je.getInstance(i).hide(),Je.getOrCreateInstance(e).toggle(this)})),V(Je),b(Je);const Ze=".bs.offcanvas",ti=".data-api",ei=`load${Ze}${ti}`,ii="show",si="showing",ni="hiding",oi=".offcanvas.show",ri=`show${Ze}`,ai=`shown${Ze}`,li=`hide${Ze}`,ci=`hidePrevented${Ze}`,hi=`hidden${Ze}`,di=`resize${Ze}`,ui=`click${Ze}${ti}`,_i=`keydown.dismiss${Ze}`,gi={backdrop:!0,keyboard:!0,scroll:!1},fi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class mi extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return gi}static get DefaultType(){return fi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,ri,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new xe).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(si),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(ii),this._element.classList.remove(si),j.trigger(this._element,ai,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,li).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ni),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(ii,ni),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new xe).reset(),j.trigger(this._element,hi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ee({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():j.trigger(this._element,ci)}:null})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_addEventListeners(){j.on(this._element,_i,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():j.trigger(this._element,ci))}))}static jQueryInterface(t){return this.each((function(){const e=mi.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)}}))}}j.on(document,ui,'[data-bs-toggle="offcanvas"]',(function(t){const e=K.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;j.one(e,hi,(()=>{h(this)&&this.focus()}));const i=K.findOne(oi);i&&i!==e&&mi.getInstance(i).hide(),mi.getOrCreateInstance(e).toggle(this)})),j.on(window,ei,(()=>{for(const t of K.find(oi))mi.getOrCreateInstance(t).show()})),j.on(window,di,(()=>{for(const t of K.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&mi.getOrCreateInstance(t).hide()})),V(mi),b(mi);const pi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],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:[]},bi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),vi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,yi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!bi.has(i)||Boolean(vi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},wi={allowList:pi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Ai={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ei={entry:"(string|element|function|null)",selector:"(string|element)"};class Ci extends q{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return wi}static get DefaultType(){return Ai}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ei)}_setContent(t,e,i){const s=K.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?l(e)?this._putElementInTemplate(c(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)yi(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return v(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Ti=new Set(["sanitize","allowList","sanitizeFn"]),ki="fade",$i="show",Si=".tooltip-inner",Li=".modal",Oi="hide.bs.modal",Ii="hover",Di="focus",Ni="click",Pi={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},xi={allowList:pi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},Mi={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ji extends W{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return xi}static get DefaultType(){return Mi}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(Li),Oi,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),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=j.trigger(this._element,this.constructor.eventName("show")),e=(u(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),j.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._queueCallback((()=>{j.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!j.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._activeTrigger[Ni]=!1,this._activeTrigger[Di]=!1,this._activeTrigger[Ii]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ki,$i),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ki),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ci({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[Si]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ki)}_isShown(){return this.tip&&this.tip.classList.contains($i)}_createPopper(t){const e=v(this._config.placement,[this,t,this._element]),s=Pi[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_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}_resolvePossibleFunction(t){return v(t,[this._element,this._element])}_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:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...v(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)j.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[Ni]=!(e._isShown()&&e._activeTrigger[Ni]),e.toggle()}));else if("manual"!==e){const t=e===Ii?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Ii?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");j.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Di:Ii]=!0,e._enter()})),j.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Di:Ii]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(Li),Oi,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=B.getDataAttributes(this._element);for(const t of Object.keys(e))Ti.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:c(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()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=ji.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(ji);const Fi=".popover-header",zi=".popover-body",Hi={...ji.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},Bi={...ji.DefaultType,content:"(null|string|element|function)"};class qi extends ji{static get Default(){return Hi}static get DefaultType(){return Bi}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[Fi]:this._getTitle(),[zi]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(qi);const Wi=".bs.scrollspy",Ri=`activate${Wi}`,Ki=`click${Wi}`,Vi=`load${Wi}.data-api`,Qi="active",Xi="[href]",Yi=".nav-link",Ui=`${Yi}, .nav-item > ${Yi}, .list-group-item`,Gi={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Ji={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Zi extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=c(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(j.off(this._config.target,Ki),j.on(this._config.target,Ki,Xi,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=K.find(Xi,this._config.target);for(const e of t){if(!e.hash||d(e))continue;const t=K.findOne(decodeURI(e.hash),this._element);h(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Qi),this._activateParents(t),j.trigger(this._element,Ri,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))K.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Qi);else for(const e of K.parents(t,".nav, .list-group"))for(const t of K.prev(e,Ui))t.classList.add(Qi)}_clearActiveClass(t){t.classList.remove(Qi);const e=K.find(`${Xi}.${Qi}`,t);for(const t of e)t.classList.remove(Qi)}static jQueryInterface(t){return this.each((function(){const e=Zi.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]()}}))}}j.on(window,Vi,(()=>{for(const t of K.find('[data-bs-spy="scroll"]'))Zi.getOrCreateInstance(t)})),b(Zi);const ts=".bs.tab",es=`hide${ts}`,is=`hidden${ts}`,ss=`show${ts}`,ns=`shown${ts}`,os=`click${ts}`,rs=`keydown${ts}`,as=`load${ts}`,ls="ArrowLeft",cs="ArrowRight",hs="ArrowUp",ds="ArrowDown",us="Home",_s="End",gs="active",fs="fade",ms="show",ps=".dropdown-toggle",bs=`:not(${ps})`,vs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',ys=`.nav-link${bs}, .list-group-item${bs}, [role="tab"]${bs}, ${vs}`,ws=`.${gs}[data-bs-toggle="tab"], .${gs}[data-bs-toggle="pill"], .${gs}[data-bs-toggle="list"]`;class As extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),j.on(this._element,rs,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?j.trigger(e,es,{relatedTarget:t}):null;j.trigger(t,ss,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(gs),this._activate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),j.trigger(t,ns,{relatedTarget:e})):t.classList.add(ms)}),t,t.classList.contains(fs)))}_deactivate(t,e){t&&(t.classList.remove(gs),t.blur(),this._deactivate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),j.trigger(t,is,{relatedTarget:e})):t.classList.remove(ms)}),t,t.classList.contains(fs)))}_keydown(t){if(![ls,cs,hs,ds,us,_s].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!d(t)));let i;if([us,_s].includes(t.key))i=e[t.key===us?0:e.length-1];else{const s=[cs,ds].includes(t.key);i=w(e,t.target,s,!0)}i&&(i.focus({preventScroll:!0}),As.getOrCreateInstance(i).show())}_getChildren(){return K.find(ys,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=K.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=K.findOne(t,i);n&&n.classList.toggle(s,e)};s(ps,gs),s(".dropdown-menu",ms),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(gs)}_getInnerElement(t){return t.matches(ys)?t:K.findOne(ys,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=As.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,os,vs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this)||As.getOrCreateInstance(this).show()})),j.on(window,as,(()=>{for(const t of K.find(ws))As.getOrCreateInstance(t)})),b(As);const Es=".bs.toast",Cs=`mouseover${Es}`,Ts=`mouseout${Es}`,ks=`focusin${Es}`,$s=`focusout${Es}`,Ss=`hide${Es}`,Ls=`hidden${Es}`,Os=`show${Es}`,Is=`shown${Es}`,Ds="hide",Ns="show",Ps="showing",xs={animation:"boolean",autohide:"boolean",delay:"number"},Ms={animation:!0,autohide:!0,delay:5e3};class js extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Ms}static get DefaultType(){return xs}static get NAME(){return"toast"}show(){j.trigger(this._element,Os).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Ds),g(this._element),this._element.classList.add(Ns,Ps),this._queueCallback((()=>{this._element.classList.remove(Ps),j.trigger(this._element,Is),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(j.trigger(this._element,Ss).defaultPrevented||(this._element.classList.add(Ps),this._queueCallback((()=>{this._element.classList.add(Ds),this._element.classList.remove(Ps,Ns),j.trigger(this._element,Ls)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ns),super.dispose()}isShown(){return this._element.classList.contains(Ns)}_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 i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,Cs,(t=>this._onInteraction(t,!0))),j.on(this._element,Ts,(t=>this._onInteraction(t,!1))),j.on(this._element,ks,(t=>this._onInteraction(t,!0))),j.on(this._element,$s,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=js.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return V(js),b(js),{Alert:U,Button:J,Carousel:Dt,Collapse:Vt,Dropdown:pe,Modal:Je,Offcanvas:mi,Popover:qi,ScrollSpy:Zi,Tab:As,Toast:js,Tooltip:ji}})); +/*! + * Bootstrap v5.3.7 (https://getbootstrap.com/) + * Copyright 2011-2025 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){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=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?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=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 L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&P(t,e,i,r.callable,r.delegationSelector)}function M(t){return t=t.replace(E,""),$[t]||t}const j={on(t,e,i,s){N(t,e,i,s,!1)},one(t,e,i,s){N(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=D(e,i,s),a=r!==e,l=O(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))x(t,l,i,e.slice(1));for(const[i,s]of Object.entries(c)){const n=i.replace(C,"");a&&!e.includes(n)||P(t,l,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;P(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=f();let n=null,o=!0,r=!0,a=!1;e!==M(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());const l=F(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function F(t,e={}){for(const[i,s]of Object.entries(e))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}function z(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function H(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const B={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${H(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${H(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=z(t.dataset[s])}return e},getDataAttribute:(t,e)=>z(t.getAttribute(`data-bs-${H(e)}`))};class q{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=l(e)?B.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...l(e)?B.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[s,n]of Object.entries(e)){const e=t[s],o=l(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(n).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${s}" provided type "${o}" but expected type "${n}".`)}var i}}class W extends q{constructor(t,e){super(),(t=c(t))&&(this._element=t,this._config=this._getConfig(e),n.set(this._element,this.constructor.DATA_KEY,this))}dispose(){n.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){y(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return n.get(c(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.3.7"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const R=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>r(t))).join(","):null},K={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 i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!d(t)&&h(t)))},getSelectorFromElement(t){const e=R(t);return e&&K.findOne(e)?e:null},getElementFromSelector(t){const e=R(t);return e?K.findOne(e):null},getMultipleElementsFromSelector(t){const e=R(t);return e?K.find(e):[]}},V=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),d(this))return;const n=K.getElementFromSelector(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()}))},Q=".bs.alert",X=`close${Q}`,Y=`closed${Q}`;class U extends W{static get NAME(){return"alert"}close(){if(j.trigger(this._element,X).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,Y),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=U.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}V(U,"close"),b(U);const G='[data-bs-toggle="button"]';class J extends W{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=J.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}j.on(document,"click.bs.button.data-api",G,(t=>{t.preventDefault();const e=t.target.closest(G);J.getOrCreateInstance(e).toggle()})),b(J);const Z=".bs.swipe",tt=`touchstart${Z}`,et=`touchmove${Z}`,it=`touchend${Z}`,st=`pointerdown${Z}`,nt=`pointerup${Z}`,ot={endCallback:null,leftCallback:null,rightCallback:null},rt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class at extends q{constructor(t,e){super(),this._element=t,t&&at.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return ot}static get DefaultType(){return rt}static get NAME(){return"swipe"}dispose(){j.off(this._element,Z)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),v(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&v(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(j.on(this._element,st,(t=>this._start(t))),j.on(this._element,nt,(t=>this._end(t))),this._element.classList.add("pointer-event")):(j.on(this._element,tt,(t=>this._start(t))),j.on(this._element,et,(t=>this._move(t))),j.on(this._element,it,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const lt=".bs.carousel",ct=".data-api",ht="ArrowLeft",dt="ArrowRight",ut="next",_t="prev",gt="left",ft="right",mt=`slide${lt}`,pt=`slid${lt}`,bt=`keydown${lt}`,vt=`mouseenter${lt}`,yt=`mouseleave${lt}`,wt=`dragstart${lt}`,At=`load${lt}${ct}`,Et=`click${lt}${ct}`,Ct="carousel",Tt="active",kt=".active",$t=".carousel-item",St=kt+$t,Lt={[ht]:ft,[dt]:gt},Ot={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},It={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Dt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=K.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Ct&&this.cycle()}static get Default(){return Ot}static get DefaultType(){return It}static get NAME(){return"carousel"}next(){this._slide(ut)}nextWhenVisible(){!document.hidden&&h(this._element)&&this.next()}prev(){this._slide(_t)}pause(){this._isSliding&&a(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?j.one(this._element,pt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,pt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?ut:_t;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&j.on(this._element,bt,(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,vt,(()=>this.pause())),j.on(this._element,yt,(()=>this._maybeEnableCycle()))),this._config.touch&&at.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of K.find(".carousel-item img",this._element))j.on(t,wt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(gt)),rightCallback:()=>this._slide(this._directionToOrder(ft)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new at(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Lt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=K.findOne(kt,this._indicatorsElement);e.classList.remove(Tt),e.removeAttribute("aria-current");const i=K.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Tt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===ut,n=e||w(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>j.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(mt).defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),g(n),i.classList.add(l),n.classList.add(l),this._queueCallback((()=>{n.classList.remove(l,c),n.classList.add(Tt),i.classList.remove(Tt,c,l),this._isSliding=!1,r(pt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return K.findOne(St,this._element)}_getItems(){return K.find($t,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===gt?_t:ut:t===gt?ut:_t}_orderToDirection(t){return p()?t===_t?gt:ft:t===_t?ft:gt}static jQueryInterface(t){return this.each((function(){const e=Dt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}j.on(document,Et,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=K.getElementFromSelector(this);if(!e||!e.classList.contains(Ct))return;t.preventDefault();const i=Dt.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===B.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),j.on(window,At,(()=>{const t=K.find('[data-bs-ride="carousel"]');for(const e of t)Dt.getOrCreateInstance(e)})),b(Dt);const Nt=".bs.collapse",Pt=`show${Nt}`,xt=`shown${Nt}`,Mt=`hide${Nt}`,jt=`hidden${Nt}`,Ft=`click${Nt}.data-api`,zt="show",Ht="collapse",Bt="collapsing",qt=`:scope .${Ht} .${Ht}`,Wt='[data-bs-toggle="collapse"]',Rt={parent:null,toggle:!0},Kt={parent:"(null|element)",toggle:"boolean"};class Vt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=K.find(Wt);for(const t of i){const e=K.getSelectorFromElement(t),i=K.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Rt}static get DefaultType(){return Kt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Vt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(j.trigger(this._element,Pt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Ht),this._element.classList.add(Bt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht,zt),this._element.style[e]="",j.trigger(this._element,xt)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,Mt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,g(this._element),this._element.classList.add(Bt),this._element.classList.remove(Ht,zt);for(const t of this._triggerArray){const e=K.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht),j.trigger(this._element,jt)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(zt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=c(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Wt);for(const e of t){const t=K.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=K.find(qt,this._config.parent);return K.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Vt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,Ft,Wt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of K.getMultipleElementsFromSelector(this))Vt.getOrCreateInstance(t,{toggle:!1}).toggle()})),b(Vt);const Qt="dropdown",Xt=".bs.dropdown",Yt=".data-api",Ut="ArrowUp",Gt="ArrowDown",Jt=`hide${Xt}`,Zt=`hidden${Xt}`,te=`show${Xt}`,ee=`shown${Xt}`,ie=`click${Xt}${Yt}`,se=`keydown${Xt}${Yt}`,ne=`keyup${Xt}${Yt}`,oe="show",re='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ae=`${re}.${oe}`,le=".dropdown-menu",ce=p()?"top-end":"top-start",he=p()?"top-start":"top-end",de=p()?"bottom-end":"bottom-start",ue=p()?"bottom-start":"bottom-end",_e=p()?"left-start":"right-start",ge=p()?"right-start":"left-start",fe={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},me={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class pe extends W{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=K.next(this._element,le)[0]||K.prev(this._element,le)[0]||K.findOne(le,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return fe}static get DefaultType(){return me}static get NAME(){return Qt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(d(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!j.trigger(this._element,te,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(oe),this._element.classList.add(oe),j.trigger(this._element,ee,t)}}hide(){if(d(this._element)||!this._isShown())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()}_completeHide(t){if(!j.trigger(this._element,Jt,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._popper&&this._popper.destroy(),this._menu.classList.remove(oe),this._element.classList.remove(oe),this._element.setAttribute("aria-expanded","false"),B.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,Zt,t),this._element.focus()}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!l(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Qt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let t=this._element;"parent"===this._config.reference?t=this._parent:l(this._config.reference)?t=c(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(oe)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return _e;if(t.classList.contains("dropstart"))return ge;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?he:ce:e?ue:de}_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(this._inNavbar||"static"===this._config.display)&&(B.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...v(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=K.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>h(t)));i.length&&w(i,e,t===Gt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=pe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=K.find(ae);for(const i of e){const e=pe.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[Ut,Gt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(re)?this:K.prev(this,re)[0]||K.next(this,re)[0]||K.findOne(re,t.delegateTarget.parentNode),o=pe.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}j.on(document,se,re,pe.dataApiKeydownHandler),j.on(document,se,le,pe.dataApiKeydownHandler),j.on(document,ie,pe.clearMenus),j.on(document,ne,pe.clearMenus),j.on(document,ie,re,(function(t){t.preventDefault(),pe.getOrCreateInstance(this).toggle()})),b(pe);const be="backdrop",ve="show",ye=`mousedown.bs.${be}`,we={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ae={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ee extends q{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return we}static get DefaultType(){return Ae}static get NAME(){return be}show(t){if(!this._config.isVisible)return void v(t);this._append();const e=this._getElement();this._config.isAnimated&&g(e),e.classList.add(ve),this._emulateAnimation((()=>{v(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(ve),this._emulateAnimation((()=>{this.dispose(),v(t)}))):v(t)}dispose(){this._isAppended&&(j.off(this._element,ye),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=c(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),j.on(t,ye,(()=>{v(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){y(t,this._getElement(),this._config.isAnimated)}}const Ce=".bs.focustrap",Te=`focusin${Ce}`,ke=`keydown.tab${Ce}`,$e="backward",Se={autofocus:!0,trapElement:null},Le={autofocus:"boolean",trapElement:"element"};class Oe extends q{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Se}static get DefaultType(){return Le}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),j.off(document,Ce),j.on(document,Te,(t=>this._handleFocusin(t))),j.on(document,ke,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,Ce))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=K.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===$e?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?$e:"forward")}}const Ie=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",De=".sticky-top",Ne="padding-right",Pe="margin-right";class xe{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,Ne,(e=>e+t)),this._setElementAttributes(Ie,Ne,(e=>e+t)),this._setElementAttributes(De,Pe,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Ne),this._resetElementAttributes(Ie,Ne),this._resetElementAttributes(De,Pe)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&B.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=B.getDataAttribute(t,e);null!==i?(B.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(l(t))e(t);else for(const i of K.find(t,this._element))e(i)}}const Me=".bs.modal",je=`hide${Me}`,Fe=`hidePrevented${Me}`,ze=`hidden${Me}`,He=`show${Me}`,Be=`shown${Me}`,qe=`resize${Me}`,We=`click.dismiss${Me}`,Re=`mousedown.dismiss${Me}`,Ke=`keydown.dismiss${Me}`,Ve=`click${Me}.data-api`,Qe="modal-open",Xe="show",Ye="modal-static",Ue={backdrop:!0,focus:!0,keyboard:!0},Ge={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Je extends W{constructor(t,e){super(t,e),this._dialog=K.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new xe,this._addEventListeners()}static get Default(){return Ue}static get DefaultType(){return Ge}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,He,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Qe),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(j.trigger(this._element,je).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Xe),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){j.off(window,Me),j.off(this._dialog,Me),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ee({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(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;const e=K.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),g(this._element),this._element.classList.add(Xe),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,Be,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){j.on(this._element,Ke,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),j.on(window,qe,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),j.on(this._element,Re,(t=>{j.one(this._element,We,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_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(Qe),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,ze)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,Fe).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Ye)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Ye),this._queueCallback((()=>{this._element.classList.remove(Ye),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Je.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,Ve,'[data-bs-toggle="modal"]',(function(t){const e=K.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,He,(t=>{t.defaultPrevented||j.one(e,ze,(()=>{h(this)&&this.focus()}))}));const i=K.findOne(".modal.show");i&&Je.getInstance(i).hide(),Je.getOrCreateInstance(e).toggle(this)})),V(Je),b(Je);const Ze=".bs.offcanvas",ti=".data-api",ei=`load${Ze}${ti}`,ii="show",si="showing",ni="hiding",oi=".offcanvas.show",ri=`show${Ze}`,ai=`shown${Ze}`,li=`hide${Ze}`,ci=`hidePrevented${Ze}`,hi=`hidden${Ze}`,di=`resize${Ze}`,ui=`click${Ze}${ti}`,_i=`keydown.dismiss${Ze}`,gi={backdrop:!0,keyboard:!0,scroll:!1},fi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class mi extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return gi}static get DefaultType(){return fi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,ri,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new xe).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(si),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(ii),this._element.classList.remove(si),j.trigger(this._element,ai,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,li).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ni),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(ii,ni),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new xe).reset(),j.trigger(this._element,hi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ee({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():j.trigger(this._element,ci)}:null})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_addEventListeners(){j.on(this._element,_i,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():j.trigger(this._element,ci))}))}static jQueryInterface(t){return this.each((function(){const e=mi.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)}}))}}j.on(document,ui,'[data-bs-toggle="offcanvas"]',(function(t){const e=K.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;j.one(e,hi,(()=>{h(this)&&this.focus()}));const i=K.findOne(oi);i&&i!==e&&mi.getInstance(i).hide(),mi.getOrCreateInstance(e).toggle(this)})),j.on(window,ei,(()=>{for(const t of K.find(oi))mi.getOrCreateInstance(t).show()})),j.on(window,di,(()=>{for(const t of K.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&mi.getOrCreateInstance(t).hide()})),V(mi),b(mi);const pi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],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:[]},bi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),vi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,yi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!bi.has(i)||Boolean(vi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},wi={allowList:pi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Ai={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ei={entry:"(string|element|function|null)",selector:"(string|element)"};class Ci extends q{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return wi}static get DefaultType(){return Ai}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ei)}_setContent(t,e,i){const s=K.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?l(e)?this._putElementInTemplate(c(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)yi(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return v(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Ti=new Set(["sanitize","allowList","sanitizeFn"]),ki="fade",$i="show",Si=".tooltip-inner",Li=".modal",Oi="hide.bs.modal",Ii="hover",Di="focus",Ni="click",Pi={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},xi={allowList:pi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},Mi={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ji extends W{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return xi}static get DefaultType(){return Mi}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(Li),Oi,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),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=j.trigger(this._element,this.constructor.eventName("show")),e=(u(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),j.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._queueCallback((()=>{j.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!j.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._activeTrigger[Ni]=!1,this._activeTrigger[Di]=!1,this._activeTrigger[Ii]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ki,$i),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ki),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ci({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[Si]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ki)}_isShown(){return this.tip&&this.tip.classList.contains($i)}_createPopper(t){const e=v(this._config.placement,[this,t,this._element]),s=Pi[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_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}_resolvePossibleFunction(t){return v(t,[this._element,this._element])}_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:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...v(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)j.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[Ni]=!(e._isShown()&&e._activeTrigger[Ni]),e.toggle()}));else if("manual"!==e){const t=e===Ii?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Ii?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");j.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Di:Ii]=!0,e._enter()})),j.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Di:Ii]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(Li),Oi,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=B.getDataAttributes(this._element);for(const t of Object.keys(e))Ti.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:c(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()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=ji.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(ji);const Fi=".popover-header",zi=".popover-body",Hi={...ji.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},Bi={...ji.DefaultType,content:"(null|string|element|function)"};class qi extends ji{static get Default(){return Hi}static get DefaultType(){return Bi}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[Fi]:this._getTitle(),[zi]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(qi);const Wi=".bs.scrollspy",Ri=`activate${Wi}`,Ki=`click${Wi}`,Vi=`load${Wi}.data-api`,Qi="active",Xi="[href]",Yi=".nav-link",Ui=`${Yi}, .nav-item > ${Yi}, .list-group-item`,Gi={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Ji={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Zi extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=c(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(j.off(this._config.target,Ki),j.on(this._config.target,Ki,Xi,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=K.find(Xi,this._config.target);for(const e of t){if(!e.hash||d(e))continue;const t=K.findOne(decodeURI(e.hash),this._element);h(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Qi),this._activateParents(t),j.trigger(this._element,Ri,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))K.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Qi);else for(const e of K.parents(t,".nav, .list-group"))for(const t of K.prev(e,Ui))t.classList.add(Qi)}_clearActiveClass(t){t.classList.remove(Qi);const e=K.find(`${Xi}.${Qi}`,t);for(const t of e)t.classList.remove(Qi)}static jQueryInterface(t){return this.each((function(){const e=Zi.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]()}}))}}j.on(window,Vi,(()=>{for(const t of K.find('[data-bs-spy="scroll"]'))Zi.getOrCreateInstance(t)})),b(Zi);const ts=".bs.tab",es=`hide${ts}`,is=`hidden${ts}`,ss=`show${ts}`,ns=`shown${ts}`,os=`click${ts}`,rs=`keydown${ts}`,as=`load${ts}`,ls="ArrowLeft",cs="ArrowRight",hs="ArrowUp",ds="ArrowDown",us="Home",_s="End",gs="active",fs="fade",ms="show",ps=".dropdown-toggle",bs=`:not(${ps})`,vs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',ys=`.nav-link${bs}, .list-group-item${bs}, [role="tab"]${bs}, ${vs}`,ws=`.${gs}[data-bs-toggle="tab"], .${gs}[data-bs-toggle="pill"], .${gs}[data-bs-toggle="list"]`;class As extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),j.on(this._element,rs,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?j.trigger(e,es,{relatedTarget:t}):null;j.trigger(t,ss,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(gs),this._activate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),j.trigger(t,ns,{relatedTarget:e})):t.classList.add(ms)}),t,t.classList.contains(fs)))}_deactivate(t,e){t&&(t.classList.remove(gs),t.blur(),this._deactivate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),j.trigger(t,is,{relatedTarget:e})):t.classList.remove(ms)}),t,t.classList.contains(fs)))}_keydown(t){if(![ls,cs,hs,ds,us,_s].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!d(t)));let i;if([us,_s].includes(t.key))i=e[t.key===us?0:e.length-1];else{const s=[cs,ds].includes(t.key);i=w(e,t.target,s,!0)}i&&(i.focus({preventScroll:!0}),As.getOrCreateInstance(i).show())}_getChildren(){return K.find(ys,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=K.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=K.findOne(t,i);n&&n.classList.toggle(s,e)};s(ps,gs),s(".dropdown-menu",ms),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(gs)}_getInnerElement(t){return t.matches(ys)?t:K.findOne(ys,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=As.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,os,vs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this)||As.getOrCreateInstance(this).show()})),j.on(window,as,(()=>{for(const t of K.find(ws))As.getOrCreateInstance(t)})),b(As);const Es=".bs.toast",Cs=`mouseover${Es}`,Ts=`mouseout${Es}`,ks=`focusin${Es}`,$s=`focusout${Es}`,Ss=`hide${Es}`,Ls=`hidden${Es}`,Os=`show${Es}`,Is=`shown${Es}`,Ds="hide",Ns="show",Ps="showing",xs={animation:"boolean",autohide:"boolean",delay:"number"},Ms={animation:!0,autohide:!0,delay:5e3};class js extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Ms}static get DefaultType(){return xs}static get NAME(){return"toast"}show(){j.trigger(this._element,Os).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Ds),g(this._element),this._element.classList.add(Ns,Ps),this._queueCallback((()=>{this._element.classList.remove(Ps),j.trigger(this._element,Is),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(j.trigger(this._element,Ss).defaultPrevented||(this._element.classList.add(Ps),this._queueCallback((()=>{this._element.classList.add(Ds),this._element.classList.remove(Ps,Ns),j.trigger(this._element,Ls)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ns),super.dispose()}isShown(){return this._element.classList.contains(Ns)}_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 i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,Cs,(t=>this._onInteraction(t,!0))),j.on(this._element,Ts,(t=>this._onInteraction(t,!1))),j.on(this._element,ks,(t=>this._onInteraction(t,!0))),j.on(this._element,$s,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=js.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return V(js),b(js),{Alert:U,Button:J,Carousel:Dt,Collapse:Vt,Dropdown:pe,Modal:Je,Offcanvas:mi,Popover:qi,ScrollSpy:Zi,Tab:As,Toast:js,Tooltip:ji}})); //# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file diff --git a/js/chart.min.js b/js/chart.min.js index 4ba6cce..a6caf62 100644 --- a/js/chart.min.js +++ b/js/chart.min.js @@ -1,14 +1,14 @@ -/*! - * Chart.js v4.5.1 - * https://www.chartjs.org - * (c) 2025 Chart.js Contributors - * Released under the MIT License - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Jo},get Decimation(){return ta},get Filler(){return ba},get Legend(){return Ma},get SubTitle(){return Pa},get Title(){return ka},get Tooltip(){return Na}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!function(t){return"symbol"==typeof t||"object"==typeof t&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const n=e.length;let o=0,a=n;if(t._sorted){const{iScale:r,vScale:l,_parsed:h}=t,c=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null,d=r.axis,{min:u,max:f,minDefined:g,maxDefined:p}=r.getUserBounds();if(g){if(o=Math.min(it(h,d,u).lo,i?n:it(e,d,r.getPixelForValue(u)).lo),c){const t=h.slice(0,o+1).reverse().findIndex((t=>!s(t[l.axis])));o-=Math.max(0,t)}o=Z(o,0,n-1)}if(p){let t=Math.max(it(h,r.axis,f,!0).hi+1,i?0:it(e,d,r.getPixelForValue(f),!0).hi+1);if(c){const e=h.slice(t-1).findIndex((t=>!s(t[l.axis])));t+=Math.max(0,e)}a=Z(t,o,n)-o}else a=n-o}return{start:o,count:a}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class xt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var bt=new xt; -/*! - * @kurkle/color v0.3.2 - * https://github.com/kurkle/color#readme - * (c) 2023 Jukka Kurkela - * Released under the MIT License - */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Jt{constructor(t){if(t instanceof Jt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Jt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Zt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Zt(t)?t:new Jt(t)}function te(t){return Zt(t)?t:new Jt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function xe(t,e){return me(t).getPropertyValue(e)}const be=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=be[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Me(t.height*s),o=Me(t.width*s);t.height=Me(t.height),t.width=Me(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=xe(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Ze(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Ze(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Ze(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Je(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Ze(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Je(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const xi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,bi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(xi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(bi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:J,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hb||l(n,x,p)&&0!==r(n,x),v=()=>!b||0===r(o,p)||l(o,x,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==x&&(b=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r!s(t[e.axis])));n.lo-=Math.max(0,a);const r=i.slice(n.hi).findIndex((t=>!s(t[e.axis])));n.hi+=Math.max(0,r)}return n}if(o._sharedOptions){const t=a[0],s="function"==typeof t.getRange&&t.getRange(e);if(s){const t=r(a,e,i-s),n=r(a,e,i+s);return{lo:t.lo,hi:n.hi}}}}return{lo:0,hi:a.length-1}}function $i(t,e,i,s,n){const o=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=o.length;t{t[a]&&t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Ki={evaluateInteractionItems:$i,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tYi(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Xi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>qi(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>qi(t,ve(e,t),"y",i.intersect,s)}};const Gi=["left","top","right","bottom"];function Ji(t,e){return t.filter((t=>t.pos===e))}function Zi(t,e){return t.filter((t=>-1===Gi.indexOf(t.pos)&&t.box.axis===e))}function Qi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function ts(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!Gi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function os(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Qi(Ji(e,"left"),!0),n=Qi(Ji(e,"right")),o=Qi(Ji(e,"top"),!0),a=Qi(Ji(e,"bottom")),r=Zi(e,"x"),l=Zi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ji(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);is(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=ts(l.concat(h),d);os(r.fullSize,g,d,p),os(l,g,d,p),os(h,g,d,p)&&os(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),rs(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,rs(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class hs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class cs extends hs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const ds="$chartjs",us={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},fs=t=>null===t||""===t;const gs=!!Se&&{passive:!0};function ps(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,gs)}function ms(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function xs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.addedNodes,s),e=e&&!ms(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function bs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.removedNodes,s),e=e&&!ms(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const _s=new Map;let ys=0;function vs(){const t=window.devicePixelRatio;t!==ys&&(ys=t,_s.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function Ms(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){_s.size||window.addEventListener("resize",vs),_s.set(t,e)}(t,o),a}function ws(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){_s.delete(t),_s.size||window.removeEventListener("resize",vs)}(t)}function ks(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=us[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,gs)}(s,e,n),n}class Ss extends hs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[ds]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",fs(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(fs(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[ds])return!1;const i=e[ds].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[ds],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:xs,detach:bs,resize:Ms}[e]||ks;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:ws,detach:ws,resize:ws}[e]||ps)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function Ps(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?cs:Ss}var Ds=Object.freeze({__proto__:null,BasePlatform:hs,BasicPlatform:cs,DomPlatform:Ss,_detectPlatform:Ps});const Cs="transparent",Os={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Cs),n=s.valid&&Qt(e||Cs);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class As{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Os[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new As(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(bt.add(this._chart,i),!0):void 0}}function Ls(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Es(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function Vs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Ws(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Ns=t=>"reset"===t||"none"===t,Hs=(t,e)=>e?t:Object.assign({},t);class js{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Is(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Ws(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Bs(t,"x")),o=e.yAxisID=l(i.yAxisID,Bs(t,"y")),a=e.rAxisID=l(i.rAxisID,Bs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Ws(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Es(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Hs(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Ts(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Ns(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Ns(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Ns(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function Ys(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for(Us(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,qs=(t,e)=>Math.min(e||t,t);function Ks(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Js(t){return t.drawTicks?t.tickLength:0}function Zs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Qs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class tn extends $s{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=Z(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Js(t.grid)-e.padding-Zs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(Z((h.highest.height+6)/o,-1,1)),Math.asin(Z(a/r,-1,1))-Math.asin(Z(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Zs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Js(n)+o):(t.height=this.maxHeight,t.width=Js(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Js(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,x=function(t){return Ae(i,t,p)};let b,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)b=x(this.bottom),w=this.bottom-u,S=b-m,D=x(t.top)+m,O=t.bottom;else if("bottom"===a)b=x(this.top),D=t.top,O=x(t.bottom)-m,w=b+m,S=this.top+u;else if("left"===a)b=x(this.right),M=this.right-u,k=b-m,P=x(t.left)+m,C=t.right;else if("right"===a)b=x(this.left),P=t.left,C=x(t.right)-m,M=b+m,k=this.left+u;else if("x"===e){if("center"===a)b=x((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=b+m,S=w+u}else if("y"===e){if("center"===a)b=x((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}M=b-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}x.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return x}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class sn{constructor(){this.controllers=new en(js,"datasets",!0),this.elements=new en($s,"elements"),this.plugins=new en(Object,"plugins"),this.scales=new en(tn,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function an(t,e){return e||!1!==t?!0===t?{}:t:null}function rn(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function ln(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function hn(t){if("x"===t||"y"===t||"r"===t)return t}function cn(t,...e){if(hn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&hn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function dn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function un(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=ln(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=cn(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return dn(t,"x",i[0])||dn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=b(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||ln(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),b(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];b(e,[ue.scales[e.type],ue.scale])})),a}function fn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=un(t,e)}function gn(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const pn=new Map,mn=new Set;function xn(t,e){let i=pn.get(t);return i||(i=e(),pn.set(t,i),mn.add(i)),i}const bn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class _n{constructor(t){this._config=function(t){return(t=t||{}).data=gn(t.data),fn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=gn(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),fn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return xn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return xn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return xn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return xn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>bn(r,t,e)))),e.forEach((t=>bn(r,s,t))),e.forEach((t=>bn(r,re[n]||{},t))),e.forEach((t=>bn(r,ue,t))),e.forEach((t=>bn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),mn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=yn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||vn(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=yn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function yn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const vn=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const Mn=["top","bottom","left","right","chartArea"];function wn(t,e){return"top"===t||"bottom"===t||-1===Mn.indexOf(t)&&"x"===e}function kn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function Sn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function Pn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Dn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Cn={},On=t=>{const e=Dn(t);return Object.values(Cn).filter((t=>t.canvas===e)).pop()};function An(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}class Tn{static defaults=ue;static instances=Cn;static overrides=re;static registry=nn;static version="4.5.1";static getChart=On;static register(...t){nn.add(...t),Ln()}static unregister(...t){nn.remove(...t),Ln()}constructor(t,e){const s=this.config=new _n(e),n=Dn(t),o=On(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||Ps(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new on,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Cn[this.id]=this,r&&l?(bt.listen(this,"complete",Sn),bt.listen(this,"progress",Pn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return nn}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return bt.stop(this),this}resize(t,e){bt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=cn(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=cn(o,n),r=l(n.type,e.dtype);void 0!==n.position&&wn(n.position,a)===wn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(nn.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{ls.configure(this,t,t.options),ls.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(kn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{ls.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){An(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;ls.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i={meta:t,index:t.index,cancelable:!0},s=Ni(this,t);!1!==this.notifyPlugins("beforeDatasetDraw",i)&&(s&&Ie(e,s),t.controller.draw(),s&&ze(e),i.cancelable=!1,this.notifyPlugins("afterDatasetDraw",i))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Ki.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),bt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Ln(){return u(Tn.instances,(t=>t._plugins.invalidate()))}function En(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Rn{static override(t){Object.assign(Rn.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return En()}parse(){return En()}format(){return En()}add(){return En()}diff(){return En()}startOf(){return En()}endOf(){return En()}}var In={_date:Rn};function zn(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Vn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data,{labels:{pointStyle:i,textAlign:s,color:n,useBorderRadius:o,borderRadius:a}}=t.legend.options;return e.labels.length&&e.datasets.length?e.labels.map(((e,r)=>{const l=t.getDatasetMeta(0).controller.getStyle(r);return{text:e,fillStyle:l.backgroundColor,fontColor:n,hidden:!t.getDataVisibility(r),lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:l.borderWidth,strokeStyle:l.borderColor,textAlign:s,pointStyle:i,borderRadius:o&&(a||l.borderRadius),index:r}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nJ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>J(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),x=g(C,h,d),b=g(C+E,c,u);s=(p-x)/2,n=(m-b)/2,o=-(p+x)/2,a=-(m+b)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),x=(i.width-o)/f,b=(i.height-o)/g,_=Math.max(Math.min(x,b)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Un=Object.freeze({__proto__:null,BarController:class extends js{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Vn(t,e,i,s)}parseArrayData(t,e,i,s){return Vn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=this._cachedMeta.controller.getParsed(e),l=r&&r[i.axis],h=t=>{const e=t._parsed.find((t=>t[i.axis]===l)),n=e&&e[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!h(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){const t=this.chart.scales,e=this.chart.options.indexAxis;return Object.keys(t).filter((i=>t[i].axis===e)).shift()}_getAxis(){const t={},e=this.getFirstScaleIdForIndexAxis();for(const i of this.chart.data.datasets)t[l("x"===this.chart.options.indexAxis?i.xAxisID:i.yAxisID,e)]=!0;return Object.keys(t)}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(x-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);x=Math.max(Math.min(x,h),o),d=x+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(x))}if(x===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;x+=t,u-=t}return{size:u,base:x,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;const c=this._getAxisCount();if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,d="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=x?g:{};if(i=b){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),x||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends $n{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:Yn,RadarController:class extends js{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>x,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),b||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Xn(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return Z(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:Z(n.innerStart,0,a),innerEnd:Z(n.innerEnd,0,a)}}function qn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function Kn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,x=n-p-f,{outerStart:b,outerEnd:_,innerStart:y,innerEnd:v}=Xn(e,u,d,x-m),M=d-b,w=d-_,k=m+b/M,S=x-_/w,P=u+y,D=u+v,O=m+y/P,A=x-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=qn(w,S,a,r);t.arc(e.x,e.y,_,S,x+E)}const i=qn(D,x,a,r);if(t.lineTo(i.x,i.y),v>0){const e=qn(D,A,a,r);t.arc(e.x,e.y,v,x+E,A+Math.PI)}const s=(x-v/u+(m+y/u))/2;if(t.arc(a,r,u,x-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=qn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=qn(M,m,a,r);if(t.lineTo(n.x,n.y),b>0){const e=qn(M,k,a,r);t.arc(e.x,e.y,b,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Gn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u,borderRadius:f}=l,g="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,g?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let p=e.endAngle;if(o){Kn(t,e,i,s,p,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,p),l.selfJoin&&p-a>=C&&0===f&&"miter"!==c&&function(t,e,i){const{startAngle:s,x:n,y:o,outerRadius:a,innerRadius:r,options:l}=e,{borderWidth:h,borderJoinStyle:c}=l,d=Math.min(h/a,G(s-i));if(t.beginPath(),t.arc(n,o,a-h/2,s+d/2,i-d/2),r>0){const e=Math.min(h/r,G(s-i));t.arc(n,o,r+h/2,i-e/2,s+e/2,!0)}else{const e=Math.min(h/2,a*G(s-i));if("round"===c)t.arc(n,o,e,i-C/2,s+C/2,!0);else if("bevel"===c){const a=2*e*e,r=-a*Math.cos(i+C/2)+n,l=-a*Math.sin(i+C/2)+o,h=a*Math.cos(s+C/2)+n,c=a*Math.sin(s+C/2)+o;t.lineTo(r,l),t.lineTo(h,c)}}t.closePath(),t.moveTo(0,0),t.rect(0,0,t.canvas.width,t.canvas.height),t.clip("evenodd")}(t,e,p),o||(Kn(t,e,i,s,p,n),t.stroke())}function Jn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Qn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[b(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[b(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=s,x=0,f=g=i),p=i}_()}function io(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?eo:to}const so="function"==typeof Path2D;function no(t,e,i,s){so&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Jn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=io(e);for(const r of n)Jn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class oo extends $s{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a),g=J(n,a,r)&&a!==r,p=f>=O||g,m=tt(o,h+u,c+u);return p&&m}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){Kn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function mo(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,x=!s(a),b=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!x&&!b)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),x&&b&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=x?a:M,w=b?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(x&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return b&&u&&w!==r?i.length&&V(i[i.length-1].value,r,xo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):b&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class _o extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const yo=t=>Math.floor(z(t)),vo=(t,e)=>Math.pow(10,yo(t)+e);function Mo(t){return 1===t/Math.pow(10,yo(t))}function wo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function ko(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=yo(e);let o=function(t,e){let i=yo(e-t);for(;wo(t,e,i)>10;)i++;for(;wo(t,e,i)<10;)i--;return Math.min(i,yo(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:Mo(g),significand:u}),s}class So extends tn{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===vo(this.min,0)?vo(this.min,-1):vo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(vo(i,-1)),o(vo(s,1)))),i<=0&&n(vo(s,-1)),s<=0&&o(vo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=ko({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function Po(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Do(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Co(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Ao(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function To(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function Lo(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Eo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(Po(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/Po(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Co(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));Lo(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash||[]),o.lineDashOffset=n.dashOffset,o.beginPath(),Eo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Io={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},zo=Object.keys(Io);function Fo(t,e){return t-e}function Vo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Bo(t,e,i,s){const n=zo.length;for(let o=zo.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function No(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class Ho extends tn{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new In._date(t.adapters.date);s.init(e),b(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Vo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Bo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=zo.length-1;o>=zo.indexOf(i);o--){const i=zo[o];if(Io[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return zo[i?zo.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=zo.indexOf(t)+1,i=zo.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=Z(s,0,o),n=Z(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Bo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var $o=Object.freeze({__proto__:null,CategoryScale:class extends tn{static id="category";static defaults={ticks:{callback:mo}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Z(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:po(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return mo.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:_o,LogarithmicScale:So,RadialLinearScale:Ro,TimeScale:Ho,TimeSeriesScale:class extends Ho{static id="timeseries";static defaults=Ho.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=jo(e,this.min),this._tableRange=jo(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(jo(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return jo(this._table,i*this._tableRange+this._minPos,!0)}}});const Yo=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Uo=Yo.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Xo(t){return Yo[t%Yo.length]}function qo(t){return Uo[t%Uo.length]}function Ko(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n instanceof Yn?e=function(t,e){return t.backgroundColor=t.data.map((()=>qo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Xo(e),t.backgroundColor=qo(e),++e}(i,e))}}function Go(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Jo={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n,a=Go(s)||(r=n)&&(r.borderColor||r.backgroundColor)||o&&Go(o)||"rgba(0,0,0,0.1)"!==ue.borderColor||"rgba(0,0,0,0.1)"!==ue.backgroundColor;var r;if(!i.forceOverride&&a)return;const l=Ko(t);s.forEach(l)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Qo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var ta={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Qo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=Z(it(e,o.axis,a).lo,0,i-1)),s=h?Z(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const x=[],b=e+i-1,_=t[e].x,y=t[b].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&x.push({...t[e],x:p}),s!==u&&s!==i&&x.push({...t[s],x:p})}o>0&&i!==u&&x.push(t[i]),x.push(a),h=e,m=0,f=g=l,c=d=u=o}}return x}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Qo(t)}};function ea(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ia(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function sa(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function na(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ia(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new oo({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function oa(t){return t&&!1!==t.fill}function aa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function ra(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function la(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&ua(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;oa(i)&&ua(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;oa(s)&&"beforeDatasetDraw"===i.drawTime&&ua(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const _a=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class ya extends $s{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=_a(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=va(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=_a(o,d),x=this.isHorizontal(),b=this._computeTitleHeight();f=x?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+b,line:0}:{x:this.left+c,y:ft(n,this.top+b+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),x?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+b+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,x?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),x)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=va(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class wa extends $s{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var ka={id:"title",_element:wa,start(t,e,i){!function(t,e){const i=new wa({ctx:t.ctx,options:e,chart:t});ls.configure(t,i,e),ls.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;ls.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa=new WeakMap;var Pa={id:"subtitle",start(t,e,i){const s=new wa({ctx:t.ctx,options:i,chart:t});ls.configure(t,s,i),ls.addBox(t,s),Sa.set(t,s)},stop(t){ls.removeBox(t,Sa.get(t)),Sa.delete(t)},beforeUpdate(t,e,i){const s=Sa.get(t);ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Da={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Aa(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Ta(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,x=0,b=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(b+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),b){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){x=Math.max(x,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),x+=p.width,{width:x,height:m}}function La(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ea(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||La(t,e,i,s),yAlign:s}}function Ra(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:Z(g,0,s.width-e.width),y:Z(p,0,s.height-e.height)}}function Ia(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function za(t){return Ca([],Oa(t))}function Fa(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const Va={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Fa(i,t);Ca(e.before,Oa(Ba(n,"beforeLabel",this,t))),Ca(e.lines,Ba(n,"label",this,t)),Ca(e.after,Oa(Ba(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return za(Ba(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Ba(i,"beforeFooter",this,t),n=Ba(i,"footer",this,t),o=Ba(i,"afterFooter",this,t);let a=[];return a=Ca(a,Oa(s)),a=Ca(a,Oa(n)),a=Ca(a,Oa(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Fa(t.callbacks,e);s.push(Ba(i,"labelColor",this,e)),n.push(Ba(i,"labelPointStyle",this,e)),o.push(Ba(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Da[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Ta(this,i),a=Object.assign({},t,e),r=Ea(this.chart,i,a),l=Ra(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,x,b,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,b=_+o,y=_-o):(p=d+f,m=p+o,b=_-o,y=_+o),x=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(b=u,_=b-o,p=m-o,x=m+o):(b=u+g,_=b+o,p=m+o,x=m-o),y=b),{x1:p,x2:m,x3:x,y1:b,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ia(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let x,b,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ia(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Da[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Ta(this,t),a=Object.assign({},i,this._size),r=Ea(e,t,a),l=Ra(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Da[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Na={id:"tooltip",_element:Wa,positioners:Da,afterInit(t,e,i){i&&(t.tooltip=new Wa({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Va},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return Tn.register(Un,$o,go,t),Tn.helpers={...Hi},Tn._adapters=In,Tn.Animation=As,Tn.Animations=Ts,Tn.animator=bt,Tn.controllers=nn.controllers.items,Tn.DatasetController=js,Tn.Element=$s,Tn.elements=go,Tn.Interaction=Ki,Tn.layouts=ls,Tn.platforms=Ds,Tn.Scale=tn,Tn.Ticks=ae,Object.assign(Tn,Un,$o,go,t,Ds),Tn.Chart=Tn,"undefined"!=typeof window&&(window.Chart=Tn),Tn})); +/*! + * Chart.js v4.5.1 + * https://www.chartjs.org + * (c) 2025 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Jo},get Decimation(){return ta},get Filler(){return ba},get Legend(){return Ma},get SubTitle(){return Pa},get Title(){return ka},get Tooltip(){return Na}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!function(t){return"symbol"==typeof t||"object"==typeof t&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const n=e.length;let o=0,a=n;if(t._sorted){const{iScale:r,vScale:l,_parsed:h}=t,c=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null,d=r.axis,{min:u,max:f,minDefined:g,maxDefined:p}=r.getUserBounds();if(g){if(o=Math.min(it(h,d,u).lo,i?n:it(e,d,r.getPixelForValue(u)).lo),c){const t=h.slice(0,o+1).reverse().findIndex((t=>!s(t[l.axis])));o-=Math.max(0,t)}o=Z(o,0,n-1)}if(p){let t=Math.max(it(h,r.axis,f,!0).hi+1,i?0:it(e,d,r.getPixelForValue(f),!0).hi+1);if(c){const e=h.slice(t-1).findIndex((t=>!s(t[l.axis])));t+=Math.max(0,e)}a=Z(t,o,n)-o}else a=n-o}return{start:o,count:a}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class xt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var bt=new xt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Jt{constructor(t){if(t instanceof Jt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Jt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Zt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Zt(t)?t:new Jt(t)}function te(t){return Zt(t)?t:new Jt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function xe(t,e){return me(t).getPropertyValue(e)}const be=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=be[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Me(t.height*s),o=Me(t.width*s);t.height=Me(t.height),t.width=Me(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=xe(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Ze(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Ze(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Ze(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Je(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Ze(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Je(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const xi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,bi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(xi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(bi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:J,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hb||l(n,x,p)&&0!==r(n,x),v=()=>!b||0===r(o,p)||l(o,x,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==x&&(b=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r!s(t[e.axis])));n.lo-=Math.max(0,a);const r=i.slice(n.hi).findIndex((t=>!s(t[e.axis])));n.hi+=Math.max(0,r)}return n}if(o._sharedOptions){const t=a[0],s="function"==typeof t.getRange&&t.getRange(e);if(s){const t=r(a,e,i-s),n=r(a,e,i+s);return{lo:t.lo,hi:n.hi}}}}return{lo:0,hi:a.length-1}}function $i(t,e,i,s,n){const o=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=o.length;t{t[a]&&t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Ki={evaluateInteractionItems:$i,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tYi(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Xi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>qi(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>qi(t,ve(e,t),"y",i.intersect,s)}};const Gi=["left","top","right","bottom"];function Ji(t,e){return t.filter((t=>t.pos===e))}function Zi(t,e){return t.filter((t=>-1===Gi.indexOf(t.pos)&&t.box.axis===e))}function Qi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function ts(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!Gi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function os(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Qi(Ji(e,"left"),!0),n=Qi(Ji(e,"right")),o=Qi(Ji(e,"top"),!0),a=Qi(Ji(e,"bottom")),r=Zi(e,"x"),l=Zi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ji(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);is(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=ts(l.concat(h),d);os(r.fullSize,g,d,p),os(l,g,d,p),os(h,g,d,p)&&os(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),rs(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,rs(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class hs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class cs extends hs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const ds="$chartjs",us={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},fs=t=>null===t||""===t;const gs=!!Se&&{passive:!0};function ps(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,gs)}function ms(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function xs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.addedNodes,s),e=e&&!ms(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function bs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.removedNodes,s),e=e&&!ms(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const _s=new Map;let ys=0;function vs(){const t=window.devicePixelRatio;t!==ys&&(ys=t,_s.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function Ms(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){_s.size||window.addEventListener("resize",vs),_s.set(t,e)}(t,o),a}function ws(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){_s.delete(t),_s.size||window.removeEventListener("resize",vs)}(t)}function ks(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=us[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,gs)}(s,e,n),n}class Ss extends hs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[ds]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",fs(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(fs(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[ds])return!1;const i=e[ds].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[ds],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:xs,detach:bs,resize:Ms}[e]||ks;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:ws,detach:ws,resize:ws}[e]||ps)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function Ps(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?cs:Ss}var Ds=Object.freeze({__proto__:null,BasePlatform:hs,BasicPlatform:cs,DomPlatform:Ss,_detectPlatform:Ps});const Cs="transparent",Os={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Cs),n=s.valid&&Qt(e||Cs);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class As{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Os[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new As(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(bt.add(this._chart,i),!0):void 0}}function Ls(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Es(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function Vs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Ws(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Ns=t=>"reset"===t||"none"===t,Hs=(t,e)=>e?t:Object.assign({},t);class js{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Is(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Ws(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Bs(t,"x")),o=e.yAxisID=l(i.yAxisID,Bs(t,"y")),a=e.rAxisID=l(i.rAxisID,Bs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Ws(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Es(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Hs(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Ts(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Ns(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Ns(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Ns(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function Ys(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for(Us(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,qs=(t,e)=>Math.min(e||t,t);function Ks(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Js(t){return t.drawTicks?t.tickLength:0}function Zs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Qs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class tn extends $s{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=Z(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Js(t.grid)-e.padding-Zs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(Z((h.highest.height+6)/o,-1,1)),Math.asin(Z(a/r,-1,1))-Math.asin(Z(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Zs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Js(n)+o):(t.height=this.maxHeight,t.width=Js(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Js(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,x=function(t){return Ae(i,t,p)};let b,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)b=x(this.bottom),w=this.bottom-u,S=b-m,D=x(t.top)+m,O=t.bottom;else if("bottom"===a)b=x(this.top),D=t.top,O=x(t.bottom)-m,w=b+m,S=this.top+u;else if("left"===a)b=x(this.right),M=this.right-u,k=b-m,P=x(t.left)+m,C=t.right;else if("right"===a)b=x(this.left),P=t.left,C=x(t.right)-m,M=b+m,k=this.left+u;else if("x"===e){if("center"===a)b=x((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=b+m,S=w+u}else if("y"===e){if("center"===a)b=x((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}M=b-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}x.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return x}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class sn{constructor(){this.controllers=new en(js,"datasets",!0),this.elements=new en($s,"elements"),this.plugins=new en(Object,"plugins"),this.scales=new en(tn,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function an(t,e){return e||!1!==t?!0===t?{}:t:null}function rn(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function ln(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function hn(t){if("x"===t||"y"===t||"r"===t)return t}function cn(t,...e){if(hn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&hn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function dn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function un(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=ln(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=cn(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return dn(t,"x",i[0])||dn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=b(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||ln(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),b(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];b(e,[ue.scales[e.type],ue.scale])})),a}function fn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=un(t,e)}function gn(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const pn=new Map,mn=new Set;function xn(t,e){let i=pn.get(t);return i||(i=e(),pn.set(t,i),mn.add(i)),i}const bn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class _n{constructor(t){this._config=function(t){return(t=t||{}).data=gn(t.data),fn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=gn(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),fn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return xn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return xn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return xn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return xn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>bn(r,t,e)))),e.forEach((t=>bn(r,s,t))),e.forEach((t=>bn(r,re[n]||{},t))),e.forEach((t=>bn(r,ue,t))),e.forEach((t=>bn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),mn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=yn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||vn(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=yn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function yn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const vn=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const Mn=["top","bottom","left","right","chartArea"];function wn(t,e){return"top"===t||"bottom"===t||-1===Mn.indexOf(t)&&"x"===e}function kn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function Sn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function Pn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Dn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Cn={},On=t=>{const e=Dn(t);return Object.values(Cn).filter((t=>t.canvas===e)).pop()};function An(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}class Tn{static defaults=ue;static instances=Cn;static overrides=re;static registry=nn;static version="4.5.1";static getChart=On;static register(...t){nn.add(...t),Ln()}static unregister(...t){nn.remove(...t),Ln()}constructor(t,e){const s=this.config=new _n(e),n=Dn(t),o=On(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||Ps(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new on,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Cn[this.id]=this,r&&l?(bt.listen(this,"complete",Sn),bt.listen(this,"progress",Pn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return nn}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return bt.stop(this),this}resize(t,e){bt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=cn(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=cn(o,n),r=l(n.type,e.dtype);void 0!==n.position&&wn(n.position,a)===wn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(nn.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{ls.configure(this,t,t.options),ls.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(kn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{ls.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){An(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;ls.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i={meta:t,index:t.index,cancelable:!0},s=Ni(this,t);!1!==this.notifyPlugins("beforeDatasetDraw",i)&&(s&&Ie(e,s),t.controller.draw(),s&&ze(e),i.cancelable=!1,this.notifyPlugins("afterDatasetDraw",i))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Ki.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),bt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Ln(){return u(Tn.instances,(t=>t._plugins.invalidate()))}function En(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Rn{static override(t){Object.assign(Rn.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return En()}parse(){return En()}format(){return En()}add(){return En()}diff(){return En()}startOf(){return En()}endOf(){return En()}}var In={_date:Rn};function zn(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Vn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data,{labels:{pointStyle:i,textAlign:s,color:n,useBorderRadius:o,borderRadius:a}}=t.legend.options;return e.labels.length&&e.datasets.length?e.labels.map(((e,r)=>{const l=t.getDatasetMeta(0).controller.getStyle(r);return{text:e,fillStyle:l.backgroundColor,fontColor:n,hidden:!t.getDataVisibility(r),lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:l.borderWidth,strokeStyle:l.borderColor,textAlign:s,pointStyle:i,borderRadius:o&&(a||l.borderRadius),index:r}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nJ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>J(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),x=g(C,h,d),b=g(C+E,c,u);s=(p-x)/2,n=(m-b)/2,o=-(p+x)/2,a=-(m+b)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),x=(i.width-o)/f,b=(i.height-o)/g,_=Math.max(Math.min(x,b)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Un=Object.freeze({__proto__:null,BarController:class extends js{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Vn(t,e,i,s)}parseArrayData(t,e,i,s){return Vn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=this._cachedMeta.controller.getParsed(e),l=r&&r[i.axis],h=t=>{const e=t._parsed.find((t=>t[i.axis]===l)),n=e&&e[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!h(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){const t=this.chart.scales,e=this.chart.options.indexAxis;return Object.keys(t).filter((i=>t[i].axis===e)).shift()}_getAxis(){const t={},e=this.getFirstScaleIdForIndexAxis();for(const i of this.chart.data.datasets)t[l("x"===this.chart.options.indexAxis?i.xAxisID:i.yAxisID,e)]=!0;return Object.keys(t)}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(x-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);x=Math.max(Math.min(x,h),o),d=x+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(x))}if(x===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;x+=t,u-=t}return{size:u,base:x,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;const c=this._getAxisCount();if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,d="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=x?g:{};if(i=b){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),x||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends $n{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:Yn,RadarController:class extends js{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>x,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),b||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Xn(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return Z(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:Z(n.innerStart,0,a),innerEnd:Z(n.innerEnd,0,a)}}function qn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function Kn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,x=n-p-f,{outerStart:b,outerEnd:_,innerStart:y,innerEnd:v}=Xn(e,u,d,x-m),M=d-b,w=d-_,k=m+b/M,S=x-_/w,P=u+y,D=u+v,O=m+y/P,A=x-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=qn(w,S,a,r);t.arc(e.x,e.y,_,S,x+E)}const i=qn(D,x,a,r);if(t.lineTo(i.x,i.y),v>0){const e=qn(D,A,a,r);t.arc(e.x,e.y,v,x+E,A+Math.PI)}const s=(x-v/u+(m+y/u))/2;if(t.arc(a,r,u,x-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=qn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=qn(M,m,a,r);if(t.lineTo(n.x,n.y),b>0){const e=qn(M,k,a,r);t.arc(e.x,e.y,b,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Gn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u,borderRadius:f}=l,g="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,g?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let p=e.endAngle;if(o){Kn(t,e,i,s,p,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,p),l.selfJoin&&p-a>=C&&0===f&&"miter"!==c&&function(t,e,i){const{startAngle:s,x:n,y:o,outerRadius:a,innerRadius:r,options:l}=e,{borderWidth:h,borderJoinStyle:c}=l,d=Math.min(h/a,G(s-i));if(t.beginPath(),t.arc(n,o,a-h/2,s+d/2,i-d/2),r>0){const e=Math.min(h/r,G(s-i));t.arc(n,o,r+h/2,i-e/2,s+e/2,!0)}else{const e=Math.min(h/2,a*G(s-i));if("round"===c)t.arc(n,o,e,i-C/2,s+C/2,!0);else if("bevel"===c){const a=2*e*e,r=-a*Math.cos(i+C/2)+n,l=-a*Math.sin(i+C/2)+o,h=a*Math.cos(s+C/2)+n,c=a*Math.sin(s+C/2)+o;t.lineTo(r,l),t.lineTo(h,c)}}t.closePath(),t.moveTo(0,0),t.rect(0,0,t.canvas.width,t.canvas.height),t.clip("evenodd")}(t,e,p),o||(Kn(t,e,i,s,p,n),t.stroke())}function Jn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Qn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[b(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[b(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=s,x=0,f=g=i),p=i}_()}function io(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?eo:to}const so="function"==typeof Path2D;function no(t,e,i,s){so&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Jn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=io(e);for(const r of n)Jn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class oo extends $s{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a),g=J(n,a,r)&&a!==r,p=f>=O||g,m=tt(o,h+u,c+u);return p&&m}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){Kn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function mo(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,x=!s(a),b=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!x&&!b)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),x&&b&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=x?a:M,w=b?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(x&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return b&&u&&w!==r?i.length&&V(i[i.length-1].value,r,xo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):b&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class _o extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const yo=t=>Math.floor(z(t)),vo=(t,e)=>Math.pow(10,yo(t)+e);function Mo(t){return 1===t/Math.pow(10,yo(t))}function wo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function ko(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=yo(e);let o=function(t,e){let i=yo(e-t);for(;wo(t,e,i)>10;)i++;for(;wo(t,e,i)<10;)i--;return Math.min(i,yo(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:Mo(g),significand:u}),s}class So extends tn{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===vo(this.min,0)?vo(this.min,-1):vo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(vo(i,-1)),o(vo(s,1)))),i<=0&&n(vo(s,-1)),s<=0&&o(vo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=ko({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function Po(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Do(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Co(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Ao(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function To(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function Lo(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Eo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(Po(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/Po(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Co(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));Lo(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash||[]),o.lineDashOffset=n.dashOffset,o.beginPath(),Eo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Io={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},zo=Object.keys(Io);function Fo(t,e){return t-e}function Vo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Bo(t,e,i,s){const n=zo.length;for(let o=zo.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function No(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class Ho extends tn{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new In._date(t.adapters.date);s.init(e),b(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Vo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Bo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=zo.length-1;o>=zo.indexOf(i);o--){const i=zo[o];if(Io[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return zo[i?zo.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=zo.indexOf(t)+1,i=zo.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=Z(s,0,o),n=Z(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Bo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var $o=Object.freeze({__proto__:null,CategoryScale:class extends tn{static id="category";static defaults={ticks:{callback:mo}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Z(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:po(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return mo.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:_o,LogarithmicScale:So,RadialLinearScale:Ro,TimeScale:Ho,TimeSeriesScale:class extends Ho{static id="timeseries";static defaults=Ho.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=jo(e,this.min),this._tableRange=jo(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(jo(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return jo(this._table,i*this._tableRange+this._minPos,!0)}}});const Yo=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Uo=Yo.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Xo(t){return Yo[t%Yo.length]}function qo(t){return Uo[t%Uo.length]}function Ko(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n instanceof Yn?e=function(t,e){return t.backgroundColor=t.data.map((()=>qo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Xo(e),t.backgroundColor=qo(e),++e}(i,e))}}function Go(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Jo={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n,a=Go(s)||(r=n)&&(r.borderColor||r.backgroundColor)||o&&Go(o)||"rgba(0,0,0,0.1)"!==ue.borderColor||"rgba(0,0,0,0.1)"!==ue.backgroundColor;var r;if(!i.forceOverride&&a)return;const l=Ko(t);s.forEach(l)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Qo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var ta={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Qo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=Z(it(e,o.axis,a).lo,0,i-1)),s=h?Z(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const x=[],b=e+i-1,_=t[e].x,y=t[b].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&x.push({...t[e],x:p}),s!==u&&s!==i&&x.push({...t[s],x:p})}o>0&&i!==u&&x.push(t[i]),x.push(a),h=e,m=0,f=g=l,c=d=u=o}}return x}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Qo(t)}};function ea(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ia(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function sa(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function na(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ia(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new oo({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function oa(t){return t&&!1!==t.fill}function aa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function ra(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function la(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&ua(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;oa(i)&&ua(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;oa(s)&&"beforeDatasetDraw"===i.drawTime&&ua(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const _a=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class ya extends $s{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=_a(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=va(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=_a(o,d),x=this.isHorizontal(),b=this._computeTitleHeight();f=x?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+b,line:0}:{x:this.left+c,y:ft(n,this.top+b+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),x?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+b+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,x?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),x)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=va(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class wa extends $s{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var ka={id:"title",_element:wa,start(t,e,i){!function(t,e){const i=new wa({ctx:t.ctx,options:e,chart:t});ls.configure(t,i,e),ls.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;ls.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa=new WeakMap;var Pa={id:"subtitle",start(t,e,i){const s=new wa({ctx:t.ctx,options:i,chart:t});ls.configure(t,s,i),ls.addBox(t,s),Sa.set(t,s)},stop(t){ls.removeBox(t,Sa.get(t)),Sa.delete(t)},beforeUpdate(t,e,i){const s=Sa.get(t);ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Da={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Aa(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Ta(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,x=0,b=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(b+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),b){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){x=Math.max(x,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),x+=p.width,{width:x,height:m}}function La(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ea(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||La(t,e,i,s),yAlign:s}}function Ra(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:Z(g,0,s.width-e.width),y:Z(p,0,s.height-e.height)}}function Ia(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function za(t){return Ca([],Oa(t))}function Fa(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const Va={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Fa(i,t);Ca(e.before,Oa(Ba(n,"beforeLabel",this,t))),Ca(e.lines,Ba(n,"label",this,t)),Ca(e.after,Oa(Ba(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return za(Ba(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Ba(i,"beforeFooter",this,t),n=Ba(i,"footer",this,t),o=Ba(i,"afterFooter",this,t);let a=[];return a=Ca(a,Oa(s)),a=Ca(a,Oa(n)),a=Ca(a,Oa(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Fa(t.callbacks,e);s.push(Ba(i,"labelColor",this,e)),n.push(Ba(i,"labelPointStyle",this,e)),o.push(Ba(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Da[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Ta(this,i),a=Object.assign({},t,e),r=Ea(this.chart,i,a),l=Ra(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,x,b,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,b=_+o,y=_-o):(p=d+f,m=p+o,b=_-o,y=_+o),x=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(b=u,_=b-o,p=m-o,x=m+o):(b=u+g,_=b+o,p=m+o,x=m-o),y=b),{x1:p,x2:m,x3:x,y1:b,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ia(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let x,b,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ia(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Da[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Ta(this,t),a=Object.assign({},i,this._size),r=Ea(e,t,a),l=Ra(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Da[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Na={id:"tooltip",_element:Wa,positioners:Da,afterInit(t,e,i){i&&(t.tooltip=new Wa({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Va},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return Tn.register(Un,$o,go,t),Tn.helpers={...Hi},Tn._adapters=In,Tn.Animation=As,Tn.Animations=Ts,Tn.animator=bt,Tn.controllers=nn.controllers.items,Tn.DatasetController=js,Tn.Element=$s,Tn.elements=go,Tn.Interaction=Ki,Tn.layouts=ls,Tn.platforms=Ds,Tn.Scale=tn,Tn.Ticks=ae,Object.assign(Tn,Un,$o,go,t,Ds),Tn.Chart=Tn,"undefined"!=typeof window&&(window.Chart=Tn),Tn})); //# sourceMappingURL=chart.umd.min.js.map \ No newline at end of file diff --git a/js/chartjs-plugin-annotation.min.js b/js/chartjs-plugin-annotation.min.js index d0d15be..726e149 100644 --- a/js/chartjs-plugin-annotation.min.js +++ b/js/chartjs-plugin-annotation.min.js @@ -1,7 +1,7 @@ -/*! -* chartjs-plugin-annotation v3.1.0 -* https://www.chartjs.org/chartjs-plugin-annotation/index - * (c) 2024 chartjs-plugin-annotation Contributors - * Released under the MIT License - */ +/*! +* chartjs-plugin-annotation v3.1.0 +* https://www.chartjs.org/chartjs-plugin-annotation/index + * (c) 2024 chartjs-plugin-annotation Contributors + * Released under the MIT License + */ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],e):(t="undefined"!=typeof globalThis?globalThis:t||self)["chartjs-plugin-annotation"]=e(t.Chart,t.Chart.helpers)}(this,(function(t,e){"use strict";const o={modes:{point:(t,e)=>i(t,e,{intersect:!0}),nearest:(t,o,n)=>function(t,o,n){let r=Number.POSITIVE_INFINITY;return i(t,o,n).reduce(((t,i)=>{const s=i.getCenterPoint(),a=function(t,e,o){if("x"===o)return{x:t.x,y:e.y};if("y"===o)return{x:e.x,y:t.y};return e}(o,s,n.axis),d=e.distanceBetweenPoints(o,a);return dt._index-e._index)).slice(0,1)}(t,o,n),x:(t,e,o)=>i(t,e,{intersect:o.intersect,axis:"x"}),y:(t,e,o)=>i(t,e,{intersect:o.intersect,axis:"y"})}};function n(t,e,n){return(o.modes[n.mode]||o.modes.nearest)(t,e,n)}function i(t,e,o){return t.filter((t=>o.intersect?t.inRange(e.x,e.y):function(t,e,o){return"x"!==o&&"y"!==o?t.inRange(e.x,e.y,"x",!0)||t.inRange(e.x,e.y,"y",!0):t.inRange(e.x,e.y,o,!0)}(t,e,o.axis)))}function r(t,e,o){const n=Math.cos(o),i=Math.sin(o),r=e.x,s=e.y;return{x:r+n*(t.x-r)-i*(t.y-s),y:s+i*(t.x-r)+n*(t.y-s)}}const s=(t,e)=>e>t||t.length>e.length&&t.slice(0,e.length)===e,a=.001,d=(t,e,o)=>Math.min(o,Math.max(e,t)),l=(t,e)=>t.value>=t.start-e&&t.value<=t.end+e;function c(t,e,o){for(const n of Object.keys(t))t[n]=d(t[n],e,o);return t}function h(t,{x:e,y:o,x2:n,y2:i},r,{borderWidth:s,hitTolerance:d}){const l=(s+d)/2,c=t.x>=e-l-a&&t.x<=n+l+a,h=t.y>=o-l-a&&t.y<=i+l+a;return"x"===r?c:("y"===r||c)&&h}function u(t,{rect:o,center:n},i,{rotation:s,borderWidth:a,hitTolerance:d}){return h(r(t,n,e.toRadians(-s)),o,i,{borderWidth:a,hitTolerance:d})}function f(t,e){const{centerX:o,centerY:n}=t.getProps(["centerX","centerY"],e);return{x:o,y:n}}const x=t=>"string"==typeof t&&t.endsWith("%"),y=t=>parseFloat(t)/100,p=t=>d(y(t),0,1),b=(t,e)=>({x:t,y:e,x2:t,y2:e,width:0,height:0}),g={box:t=>b(t.centerX,t.centerY),doughnutLabel:t=>b(t.centerX,t.centerY),ellipse:t=>({centerX:t.centerX,centerY:t.centerX,radius:0,width:0,height:0}),label:t=>b(t.centerX,t.centerY),line:t=>b(t.x,t.y),point:t=>({centerX:t.centerX,centerY:t.centerY,radius:0,width:0,height:0}),polygon:t=>b(t.centerX,t.centerY)};function m(t,e){return"start"===e?0:"end"===e?t:x(e)?p(e)*t:t/2}function v(t,e,o=!0){return"number"==typeof e?e:x(e)?(o?p(e):y(e))*t:t}function w(t,o,{borderWidth:n,position:i,xAdjust:r,yAdjust:s},a){const d=e.isObject(a),l=o.width+(d?a.width:0)+n,c=o.height+(d?a.height:0)+n,h=M(i),u=k(t.x,l,r,h.x),f=k(t.y,c,s,h.y);return{x:u,y:f,x2:u+l,y2:f+c,width:l,height:c,centerX:u+l/2,centerY:f+c/2}}function M(t,o="center"){return e.isObject(t)?{x:e.valueOrDefault(t.x,o),y:e.valueOrDefault(t.y,o)}:{x:t=e.valueOrDefault(t,o),y:t}}const P=(t,e)=>t&&t.autoFit&&e<1;function S(t,o){const n=t.font,i=e.isArray(n)?n:[n];return P(t,o)?i.map((function(t){const n=e.toFont(t);return n.size=Math.floor(t.size*o),n.lineHeight=t.lineHeight,e.toFont(n)})):i.map((t=>e.toFont(t)))}function C(t){return t&&(e.defined(t.xValue)||e.defined(t.yValue))}function k(t,e,o=0,n){return t-m(e,n)+o}function A(t,o,n){const i=n.init;if(i)return!0===i?D(o,n):function(t,o,n){const i=e.callback(n.init,[{chart:t,properties:o,options:n}]);if(!0===i)return D(o,n);if(e.isObject(i))return i}(t,o,n)}function T(t,o,n){let i=!1;return o.forEach((o=>{e.isFunction(t[o])?(i=!0,n[o]=t[o]):e.defined(n[o])&&delete n[o]})),i}function D(t,e){const o=e.type||"line";return g[o](t)}const j=new Map,O=t=>isNaN(t)||t<=0,R=t=>t.reduce((function(t,e){return t+=e.string}),"");function I(t){if(t&&"object"==typeof t){const e=t.toString();return"[object HTMLImageElement]"===e||"[object HTMLCanvasElement]"===e}}function Y(t,{x:o,y:n},i){i&&(t.translate(o,n),t.rotate(e.toRadians(i)),t.translate(-o,-n))}function X(t,e){if(e&&e.borderWidth)return t.lineCap=e.borderCapStyle||"butt",t.setLineDash(e.borderDash),t.lineDashOffset=e.borderDashOffset,t.lineJoin=e.borderJoinStyle||"miter",t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,!0}function _(t,e){t.shadowColor=e.backgroundShadowColor,t.shadowBlur=e.shadowBlur,t.shadowOffsetX=e.shadowOffsetX,t.shadowOffsetY=e.shadowOffsetY}function E(t,o){const n=o.content;if(I(n)){return{width:v(n.width,o.width),height:v(n.height,o.height)}}const i=S(o),r=o.textStrokeWidth,s=e.isArray(n)?n:[n],a=s.join()+R(i)+r+(t._measureText?"-spriting":"");return j.has(a)||j.set(a,function(t,e,o,n){t.save();const i=e.length;let r=0,s=n;for(let a=0;a0)return t.lineJoin="round",t.miterLimit=2,t.lineWidth=e.textStrokeWidth,t.strokeStyle=e.textStrokeColor,!0}(t,n)&&function(t,{x:e,y:o},n,i){t.beginPath();let r=0;n.forEach((function(n,s){const a=i[Math.min(s,i.length-1)],d=a.lineHeight;t.font=a.string,t.strokeText(n,e,o+d/2+r),r+=d})),t.stroke()}(t,{x:h,y:u},s,a),function(t,{x:e,y:o},n,{fonts:i,colors:r}){let s=0;n.forEach((function(n,a){const d=r[Math.min(a,r.length-1)],l=i[Math.min(a,i.length-1)],c=l.lineHeight;t.beginPath(),t.font=l.string,t.fillStyle=d,t.fillText(n,e,o+c/2+s),s+=c,t.fill()}))}(t,{x:h,y:u},s,{fonts:a,colors:c}),t.restore()}function F(t,o,n,i){const{radius:r,options:s}=o,a=s.pointStyle,d=s.rotation;let l=(d||0)*e.RAD_PER_DEG;if(I(a))return t.save(),t.translate(n,i),t.rotate(l),t.drawImage(a,-a.width/2,-a.height/2,a.width,a.height),void t.restore();O(r)||function(t,{x:o,y:n,radius:i,rotation:r,style:s,rad:a}){let d,l,c,h;switch(t.beginPath(),s){default:t.arc(o,n,i,0,e.TAU),t.closePath();break;case"triangle":t.moveTo(o+Math.sin(a)*i,n-Math.cos(a)*i),a+=e.TWO_THIRDS_PI,t.lineTo(o+Math.sin(a)*i,n-Math.cos(a)*i),a+=e.TWO_THIRDS_PI,t.lineTo(o+Math.sin(a)*i,n-Math.cos(a)*i),t.closePath();break;case"rectRounded":h=.516*i,c=i-h,d=Math.cos(a+e.QUARTER_PI)*c,l=Math.sin(a+e.QUARTER_PI)*c,t.arc(o-d,n-l,h,a-e.PI,a-e.HALF_PI),t.arc(o+l,n-d,h,a-e.HALF_PI,a),t.arc(o+d,n+l,h,a,a+e.HALF_PI),t.arc(o-l,n+d,h,a+e.HALF_PI,a+e.PI),t.closePath();break;case"rect":if(!r){c=Math.SQRT1_2*i,t.rect(o-c,n-c,2*c,2*c);break}a+=e.QUARTER_PI;case"rectRot":d=Math.cos(a)*i,l=Math.sin(a)*i,t.moveTo(o-d,n-l),t.lineTo(o+l,n-d),t.lineTo(o+d,n+l),t.lineTo(o-l,n+d),t.closePath();break;case"crossRot":a+=e.QUARTER_PI;case"cross":d=Math.cos(a)*i,l=Math.sin(a)*i,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l),t.moveTo(o+l,n-d),t.lineTo(o-l,n+d);break;case"star":d=Math.cos(a)*i,l=Math.sin(a)*i,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l),t.moveTo(o+l,n-d),t.lineTo(o-l,n+d),a+=e.QUARTER_PI,d=Math.cos(a)*i,l=Math.sin(a)*i,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l),t.moveTo(o+l,n-d),t.lineTo(o-l,n+d);break;case"line":d=Math.cos(a)*i,l=Math.sin(a)*i,t.moveTo(o-d,n-l),t.lineTo(o+d,n+l);break;case"dash":t.moveTo(o,n),t.lineTo(o+Math.cos(a)*i,n+Math.sin(a)*i)}t.fill()}(t,{x:n,y:i,radius:r,rotation:d,style:a,rad:l})}const H=["left","bottom","top","right"];function N(t,o){const{pointX:n,pointY:i,options:s}=o,a=s.callout,d=a&&a.display&&function(t,o){const n=o.position;if(H.includes(n))return n;return function(t,o){const{x:n,y:i,x2:s,y2:a,width:d,height:l,pointX:c,pointY:h,centerX:u,centerY:f,rotation:x}=t,y={x:u,y:f},p=o.start,b=v(d,p),g=v(l,p),m=[n,n+b,n+b,s],w=[i+g,a,i,a],M=[];for(let t=0;t<4;t++){const o=r({x:m[t],y:w[t]},y,e.toRadians(x));M.push({position:H[t],distance:e.distanceBetweenPoints(o,{x:c,y:h})})}return M.sort(((t,e)=>t.distance-e.distance))[0].position}(t,o)}(o,a);if(!d||function(t,e,o){const{pointX:n,pointY:i}=t,r=e.margin;let s=n,a=i;"left"===o?s+=r:"right"===o?s-=r:"top"===o?a+=r:"bottom"===o&&(a-=r);return t.inRange(s,a)}(o,a,d))return;t.save(),t.beginPath();if(!X(t,a))return t.restore();const{separatorStart:l,separatorEnd:c}=function(t,e){const{x:o,y:n,x2:i,y2:r}=t,s=function(t,e){const{width:o,height:n,options:i}=t,r=i.callout.margin+i.borderWidth/2;if("right"===e)return o+r;if("bottom"===e)return n+r;return-r}(t,e);let a,d;"left"===e||"right"===e?(a={x:o+s,y:n},d={x:a.x,y:r}):(a={x:o,y:n+s},d={x:i,y:a.y});return{separatorStart:a,separatorEnd:d}}(o,d),{sideStart:h,sideEnd:u}=function(t,e,o){const{y:n,width:i,height:r,options:s}=t,a=s.callout.start,d=function(t,e){const o=e.side;if("left"===t||"top"===t)return-o;return o}(e,s.callout);let l,c;"left"===e||"right"===e?(l={x:o.x,y:n+v(r,a)},c={x:l.x+d,y:l.y}):(l={x:o.x+v(i,a),y:o.y},c={x:l.x,y:l.y+d});return{sideStart:l,sideEnd:c}}(o,d,l);(a.margin>0||0===s.borderWidth)&&(t.moveTo(l.x,l.y),t.lineTo(c.x,c.y)),t.moveTo(h.x,h.y),t.lineTo(u.x,u.y);const f=r({x:n,y:i},o.getCenterPoint(),e.toRadians(-o.rotation));t.lineTo(f.x,f.y),t.stroke(),t.restore()}const L={xScaleID:{min:"xMin",max:"xMax",start:"left",end:"right",startProp:"x",endProp:"x2"},yScaleID:{min:"yMin",max:"yMax",start:"bottom",end:"top",startProp:"y",endProp:"y2"}};function V(t,o,n){return o="number"==typeof o?o:t.parse(o),e.isFinite(o)?t.getPixelForValue(o):n}function B(t,e,o){const n=e[o];if(n||"scaleID"===o)return n;const i=o.charAt(0),r=Object.values(t).filter((t=>t.axis&&t.axis===i));return r.length?r[0].id:i}function $(t,e){if(t){const o=t.options.reverse;return{start:V(t,e.min,o?e.end:e.start),end:V(t,e.max,o?e.start:e.end)}}}function U(t,e){const{chartArea:o,scales:n}=t,i=n[B(n,e,"xScaleID")],r=n[B(n,e,"yScaleID")];let s=o.width/2,a=o.height/2;return i&&(s=V(i,e.xValue,i.left+i.width/2)),r&&(a=V(r,e.yValue,r.top+r.height/2)),{x:s,y:a}}function J(t,e){const o=t.scales,n=o[B(o,e,"xScaleID")],i=o[B(o,e,"yScaleID")];if(!n&&!i)return{};let{left:r,right:s}=n||t.chartArea,{top:a,bottom:d}=i||t.chartArea;const l=K(n,{min:e.xMin,max:e.xMax,start:r,end:s});r=l.start,s=l.end;const c=K(i,{min:e.yMin,max:e.yMax,start:d,end:a});return a=c.start,d=c.end,{x:r,y:a,x2:s,y2:d,width:s-r,height:d-a,centerX:r+(s-r)/2,centerY:a+(d-a)/2}}function q(t,e){if(!C(e)){const o=J(t,e);let n=e.radius;n&&!isNaN(n)||(n=Math.min(o.width,o.height)/2,e.radius=n);const i=2*n,r=o.centerX+e.xAdjust,s=o.centerY+e.yAdjust;return{x:r-n,y:s-n,x2:r+n,y2:s+n,centerX:r,centerY:s,width:i,height:i,radius:n}}return function(t,e){const o=U(t,e),n=2*e.radius;return{x:o.x-e.radius+e.xAdjust,y:o.y-e.radius+e.yAdjust,x2:o.x+e.radius+e.xAdjust,y2:o.y+e.radius+e.yAdjust,centerX:o.x+e.xAdjust,centerY:o.y+e.yAdjust,radius:e.radius,width:n,height:n}}(t,e)}function Q(t,e){const{scales:o,chartArea:n}=t,i=o[e.scaleID],r={x:n.left,y:n.top,x2:n.right,y2:n.bottom};return i?function(t,e,o){const n=V(t,o.value,NaN),i=V(t,o.endValue,n);t.isHorizontal()?(e.x=n,e.x2=i):(e.y=n,e.y2=i)}(i,r,e):function(t,e,o){for(const n of Object.keys(L)){const i=t[B(t,o,n)];if(i){const{min:t,max:r,start:s,end:a,startProp:d,endProp:l}=L[n],c=$(i,{min:o[t],max:o[r],start:i[s],end:i[a]});e[d]=c.start,e[l]=c.end}}}(o,r,e),r}function G(t,e){const o=J(t,e);return o.initProperties=A(t,o,e),o.elements=[{type:"label",optionScope:"label",properties:tt(t,o,e),initProperties:o.initProperties}],o}function K(t,e){const o=$(t,e)||e;return{start:Math.min(o.start,o.end),end:Math.max(o.start,o.end)}}function Z(t,e){const{start:o,end:n,borderWidth:i}=t,{position:r,padding:{start:s,end:a},adjust:d}=e;return o+i/2+d+m(n-i-o-s-a-e.size,r)}function tt(t,o,n){const i=n.label;i.backgroundColor="transparent",i.callout.display=!1;const r=M(i.position),s=e.toPadding(i.padding),a=E(t.ctx,i),d=function({properties:t,options:e},o,n,i){const{x:r,x2:s,width:a}=t;return Z({start:r,end:s,size:a,borderWidth:e.borderWidth},{position:n.x,padding:{start:i.left,end:i.right},adjust:e.label.xAdjust,size:o.width})}({properties:o,options:n},a,r,s),l=function({properties:t,options:e},o,n,i){const{y:r,y2:s,height:a}=t;return Z({start:r,end:s,size:a,borderWidth:e.borderWidth},{position:n.y,padding:{start:i.top,end:i.bottom},adjust:e.label.yAdjust,size:o.height})}({properties:o,options:n},a,r,s),c=a.width+s.width,h=a.height+s.height;return{x:d,y:l,x2:d+c,y2:l+h,width:c,height:h,centerX:d+c/2,centerY:l+h/2,rotation:i.rotation}}const et=["enter","leave"],ot=et.concat("click");function nt(t,e,o){if(t.listened)switch(e.type){case"mousemove":case"mouseout":return function(t,e,o){if(!t.moveListened)return;let i;i="mousemove"===e.type?n(t.visibleElements,e,o.interaction):[];const r=t.hovered;t.hovered=i;const s={state:t,event:e};let a=it(s,"leave",r,i);return it(s,"enter",i,r)||a}(t,e,o);case"click":return function(t,e,o){const i=t.listeners,r=n(t.visibleElements,e,o.interaction);let s;for(const t of r)s=rt(t.options.click||i.click,t,e)||s;return s}(t,e,o)}}function it({state:t,event:e},o,n,i){let r;for(const s of n)i.indexOf(s)<0&&(r=rt(s.options[o]||t.listeners[o],s,e)||r);return r}function rt(t,o,n){return!0===e.callback(t,[o.$context,n])}const st=["afterDraw","beforeDraw"];function at(t,o,n){if(t.hooked){const i=o.options[n]||t.hooks[n];return e.callback(i,[o.$context])}}function dt(t,o,n){const i=function(t,o,n){const i=o.axis,r=o.id,s=i+"ScaleID",a={min:e.valueOrDefault(o.min,Number.NEGATIVE_INFINITY),max:e.valueOrDefault(o.max,Number.POSITIVE_INFINITY)};for(const e of n)e.scaleID===r?ut(e,o,["value","endValue"],a):B(t,e,s)===r&&ut(e,o,[i+"Min",i+"Max",i+"Value"],a);return a}(t.scales,o,n);let r=lt(o,i,"min","suggestedMin");r=lt(o,i,"max","suggestedMax")||r,r&&e.isFunction(o.handleTickRangeOptions)&&o.handleTickRangeOptions()}function lt(t,o,n,i){if(e.isFinite(o[n])&&!function(t,o,n){return e.defined(t[o])||e.defined(t[n])}(t.options,n,i)){const e=t[n]!==o[n];return t[n]=o[n],e}}function ct(t,e){for(const o of["scaleID","xScaleID","yScaleID"]){const n=B(e,t,o);n&&!e[n]&&ht(t,o)&&console.warn(`No scale found with id '${n}' for annotation '${t.id}'`)}}function ht(t,o){if("scaleID"===o)return!0;const n=o.charAt(0);for(const o of["Min","Max","Value"])if(e.defined(t[n+o]))return!0;return!1}function ut(t,o,n,i){for(const r of n){const n=t[r];if(e.defined(n)){const t=o.parse(n);i.min=Math.min(i.min,t),i.max=Math.max(i.max,t)}}}class ft extends t.Element{inRange(t,o,n,i){const{x:s,y:a}=r({x:t,y:o},this.getCenterPoint(i),e.toRadians(-this.options.rotation));return h({x:s,y:a},this.getProps(["x","y","x2","y2"],i),n,this.options)}getCenterPoint(t){return f(this,t)}draw(t){t.save(),Y(t,this.getCenterPoint(),this.options.rotation),W(t,this,this.options),t.restore()}get label(){return this.elements&&this.elements[0]}resolveElementProperties(t,e){return G(t,e)}}ft.id="boxAnnotation",ft.defaults={adjustScaleRange:!0,backgroundShadowColor:"transparent",borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderRadius:0,borderShadowColor:"transparent",borderWidth:1,display:!0,init:void 0,hitTolerance:0,label:{backgroundColor:"transparent",borderWidth:0,callout:{display:!1},color:"black",content:null,display:!1,drawTime:void 0,font:{family:void 0,lineHeight:void 0,size:void 0,style:void 0,weight:"bold"},height:void 0,hitTolerance:void 0,opacity:void 0,padding:6,position:"center",rotation:void 0,textAlign:"start",textStrokeColor:void 0,textStrokeWidth:0,width:void 0,xAdjust:0,yAdjust:0,z:void 0},rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,xMax:void 0,xMin:void 0,xScaleID:void 0,yMax:void 0,yMin:void 0,yScaleID:void 0,z:0},ft.defaultRoutes={borderColor:"color",backgroundColor:"color"},ft.descriptors={label:{_fallback:!0}};class xt extends t.Element{inRange(t,e,o,n){return u({x:t,y:e},{rect:this.getProps(["x","y","x2","y2"],n),center:this.getCenterPoint(n)},o,{rotation:this.rotation,borderWidth:0,hitTolerance:this.options.hitTolerance})}getCenterPoint(t){return f(this,t)}draw(t){const e=this.options;e.display&&e.content&&(!function(t,e){const{_centerX:o,_centerY:n,_radius:i,_startAngle:r,_endAngle:s,_counterclockwise:a,options:d}=e;t.save();const l=X(t,d);t.fillStyle=d.backgroundColor,t.beginPath(),t.arc(o,n,i,r,s,a),t.closePath(),t.fill(),l&&t.stroke();t.restore()}(t,this),t.save(),Y(t,this.getCenterPoint(),this.rotation),z(t,this,e,this._fitRatio),t.restore())}resolveElementProperties(o,n){const i=function(e,o){return e.getSortedVisibleDatasetMetas().reduce((function(n,i){const r=i.controller;return r instanceof t.DoughnutController&&function(t,e,o){if(!e.autoHide)return!0;for(let e=0;e=90?i:n}),void 0)}(o,n);if(!i)return{};const{controllerMeta:r,point:s,radius:a}=function({chartArea:t},o,n){const{left:i,top:r,right:s,bottom:a}=t,{innerRadius:d,offsetX:l,offsetY:c}=n.controller,h=(i+s)/2+l,u=(r+a)/2+c,f={left:Math.max(h-d,i),right:Math.min(h+d,s),top:Math.max(u-d,r),bottom:Math.min(u+d,a)},x={x:(f.left+f.right)/2,y:(f.top+f.bottom)/2},y=o.spacing+o.borderWidth/2,p=d-y,b=x.y>u,g=function(t,o,n,i){const r=Math.pow(n-t,2),s=Math.pow(i,2),a=-2*o,d=Math.pow(o,2)+r-s,l=Math.pow(a,2)-4*d;if(l<=0)return{_startAngle:0,_endAngle:e.TAU};const c=(-a-Math.sqrt(l))/2,h=(-a+Math.sqrt(l))/2;return{_startAngle:e.getAngleFromPoint({x:o,y:n},{x:c,y:t}).angle,_endAngle:e.getAngleFromPoint({x:o,y:n},{x:h,y:t}).angle}}(b?r+y:a-y,h,u,p),m={_centerX:h,_centerY:u,_radius:p,_counterclockwise:b,...g};return{controllerMeta:m,point:x,radius:Math.min(d,Math.min(f.right-f.left,f.bottom-f.top)/2)}}(o,n,i);let d=E(o.ctx,n);const l=function({width:t,height:e},o){const n=Math.sqrt(Math.pow(t,2)+Math.pow(e,2));return 2*o/n}(d,a);P(n,l)&&(d={width:d.width*l,height:d.height*l});const{position:c,xAdjust:h,yAdjust:u}=n,f=w(s,d,{borderWidth:0,position:c,xAdjust:h,yAdjust:u});return{initProperties:A(o,f,n),...f,...r,rotation:n.rotation,_fitRatio:l}}}xt.id="doughnutLabelAnnotation",xt.defaults={autoFit:!0,autoHide:!0,backgroundColor:"transparent",backgroundShadowColor:"transparent",borderColor:"transparent",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderShadowColor:"transparent",borderWidth:0,color:"black",content:null,display:!0,font:{family:void 0,lineHeight:void 0,size:void 0,style:void 0,weight:void 0},height:void 0,hitTolerance:0,init:void 0,opacity:void 0,position:"center",rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,spacing:1,textAlign:"center",textStrokeColor:void 0,textStrokeWidth:0,width:void 0,xAdjust:0,yAdjust:0},xt.defaultRoutes={};class yt extends t.Element{inRange(t,e,o,n){return u({x:t,y:e},{rect:this.getProps(["x","y","x2","y2"],n),center:this.getCenterPoint(n)},o,{rotation:this.rotation,borderWidth:this.options.borderWidth,hitTolerance:this.options.hitTolerance})}getCenterPoint(t){return f(this,t)}draw(t){const o=this.options,n=!e.defined(this._visible)||this._visible;o.display&&o.content&&n&&(t.save(),Y(t,this.getCenterPoint(),this.rotation),N(t,this),W(t,this,o),z(t,function({x:t,y:o,width:n,height:i,options:r}){const s=r.borderWidth/2,a=e.toPadding(r.padding);return{x:t+a.left+s,y:o+a.top+s,width:n-a.left-a.right-r.borderWidth,height:i-a.top-a.bottom-r.borderWidth}}(this),o),t.restore())}resolveElementProperties(t,o){let n;if(C(o))n=U(t,o);else{const{centerX:e,centerY:i}=J(t,o);n={x:e,y:i}}const i=e.toPadding(o.padding),r=w(n,E(t.ctx,o),o,i);return{initProperties:A(t,r,o),pointX:n.x,pointY:n.y,...r,rotation:o.rotation}}}yt.id="labelAnnotation",yt.defaults={adjustScaleRange:!0,backgroundColor:"transparent",backgroundShadowColor:"transparent",borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderRadius:0,borderShadowColor:"transparent",borderWidth:0,callout:{borderCapStyle:"butt",borderColor:void 0,borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:1,display:!1,margin:5,position:"auto",side:5,start:"50%"},color:"black",content:null,display:!0,font:{family:void 0,lineHeight:void 0,size:void 0,style:void 0,weight:void 0},height:void 0,hitTolerance:0,init:void 0,opacity:void 0,padding:6,position:"center",rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,textAlign:"center",textStrokeColor:void 0,textStrokeWidth:0,width:void 0,xAdjust:0,xMax:void 0,xMin:void 0,xScaleID:void 0,xValue:void 0,yAdjust:0,yMax:void 0,yMin:void 0,yScaleID:void 0,yValue:void 0,z:0},yt.defaultRoutes={borderColor:"color"};const pt=(t,e,o)=>({x:t.x+o*(e.x-t.x),y:t.y+o*(e.y-t.y)}),bt=(t,e,o)=>pt(e,o,Math.abs((t-e.y)/(o.y-e.y))).x,gt=(t,e,o)=>pt(e,o,Math.abs((t-e.x)/(o.x-e.x))).y,mt=t=>t*t,vt=(t,e,{x:o,y:n,x2:i,y2:r},s)=>"y"===s?{start:Math.min(n,r),end:Math.max(n,r),value:e}:{start:Math.min(o,i),end:Math.max(o,i),value:t},wt=(t,e,o,n)=>(1-n)*(1-n)*t+2*(1-n)*n*e+n*n*o,Mt=(t,e,o,n)=>({x:wt(t.x,e.x,o.x,n),y:wt(t.y,e.y,o.y,n)}),Pt=(t,e,o,n)=>2*(1-n)*(e-t)+2*n*(o-e),St=(t,o,n,i)=>-Math.atan2(Pt(t.x,o.x,n.x,i),Pt(t.y,o.y,n.y,i))+.5*e.PI;class Ct extends t.Element{inRange(t,e,o,n){const i=(this.options.borderWidth+this.options.hitTolerance)/2;if("x"!==o&&"y"!==o){const o={mouseX:t,mouseY:e},{path:r,ctx:s}=this;if(r){X(s,this.options),s.lineWidth+=this.options.hitTolerance;const{chart:i}=this.$context,a=t*i.currentDevicePixelRatio,d=e*i.currentDevicePixelRatio,l=s.isPointInStroke(r,a,d)||Tt(this,o,n);return s.restore(),l}return function(t,{mouseX:e,mouseY:o},n=a,i){const{x:r,y:s,x2:d,y2:l}=t.getProps(["x","y","x2","y2"],i),c=d-r,h=l-s,u=mt(c)+mt(h),f=0===u?-1:((e-r)*c+(o-s)*h)/u;let x,y;f<0?(x=r,y=s):f>1?(x=d,y=l):(x=r+f*c,y=s+f*h);return mt(e-x)+mt(o-y)<=n}(this,o,mt(i),n)||Tt(this,o,n)}return function(t,{mouseX:e,mouseY:o},n,{hitSize:i,useFinalPosition:r}){const s=vt(e,o,t.getProps(["x","y","x2","y2"],r),n);return l(s,i)||Tt(t,{mouseX:e,mouseY:o},r,n)}(this,{mouseX:t,mouseY:e},o,{hitSize:i,useFinalPosition:n})}getCenterPoint(t){return f(this,t)}draw(t){const{x:o,y:n,x2:i,y2:r,cp:s,options:a}=this;if(t.save(),!X(t,a))return t.restore();_(t,a);const d=Math.sqrt(Math.pow(i-o,2)+Math.pow(r-n,2));if(a.curve&&s)return function(t,o,n,i){const{x:r,y:s,x2:a,y2:d,options:l}=o,{startOpts:c,endOpts:h,startAdjust:u,endAdjust:f}=Ot(o),x={x:r,y:s},y={x:a,y:d},p=St(x,n,y,0),b=St(x,n,y,1)-e.PI,g=Mt(x,n,y,u/i),m=Mt(x,n,y,1-f/i),v=new Path2D;t.beginPath(),v.moveTo(g.x,g.y),v.quadraticCurveTo(n.x,n.y,m.x,m.y),t.shadowColor=l.borderShadowColor,t.stroke(v),o.path=v,o.ctx=t,Yt(t,g,{angle:p,adjust:u},c),Yt(t,m,{angle:b,adjust:f},h)}(t,this,s,d),t.restore();const{startOpts:l,endOpts:c,startAdjust:h,endAdjust:u}=Ot(this),f=Math.atan2(r-n,i-o);t.translate(o,n),t.rotate(f),t.beginPath(),t.moveTo(0+h,0),t.lineTo(d-u,0),t.shadowColor=a.borderShadowColor,t.stroke(),It(t,0,h,l),It(t,d,-u,c),t.restore()}get label(){return this.elements&&this.elements[0]}resolveElementProperties(t,o){const n=Q(t,o),{x:i,y:s,x2:a,y2:d}=n,l=function({x:t,y:e,x2:o,y2:n},{top:i,right:r,bottom:s,left:a}){return!(tr&&o>r||es&&n>s)}(n,t.chartArea),c=l?function(t,e,o){const{x:n,y:i}=At(t,e,o),{x:r,y:s}=At(e,t,o);return{x:n,y:i,x2:r,y2:s,width:Math.abs(r-n),height:Math.abs(s-i)}}({x:i,y:s},{x:a,y:d},t.chartArea):{x:i,y:s,x2:a,y2:d,width:Math.abs(a-i),height:Math.abs(d-s)};if(c.centerX=(a+i)/2,c.centerY=(d+s)/2,c.initProperties=A(t,c,o),o.curve){const t={x:c.x,y:c.y},n={x:c.x2,y:c.y2};c.cp=function(t,e,o){const{x:n,y:i,x2:s,y2:a,centerX:d,centerY:l}=t,c=Math.atan2(a-i,s-n),h=M(e.controlPoint,0);return r({x:d+v(o,h.x,!1),y:l+v(o,h.y,!1)},{x:d,y:l},c)}(c,o,e.distanceBetweenPoints(t,n))}const h=function(t,o,n){const i=n.borderWidth,r=e.toPadding(n.padding),s=E(t.ctx,n),a=s.width+r.width+i,d=s.height+r.height+i;return function(t,o,n,i){const{width:r,height:s,padding:a}=n,{xAdjust:d,yAdjust:l}=o,c={x:t.x,y:t.y},h={x:t.x2,y:t.y2},u="auto"===o.rotation?function(t){const{x:o,y:n,x2:i,y2:r}=t,s=Math.atan2(r-n,i-o);return s>e.PI/2?s-e.PI:si&&(e=gt(i,{x:t,y:e},o),t=i),er&&(t=bt(r,{x:t,y:e},o),e=r),{x:t,y:e}}function Tt(t,{mouseX:e,mouseY:o},n,i){const r=t.label;return r.options.display&&r.inRange(e,o,i,n)}function Dt(t,e,o,n){const{labelSize:i,padding:r}=e,s=t.w*n.dx,a=t.h*n.dy,l=s>0&&(i.w/2+r.left-n.x)/s,c=a>0&&(i.h/2+r.top-n.y)/a;return d(Math.max(l,c),0,.25)}function jt(t,e){const{size:o,min:n,max:i,padding:r}=e,s=o/2;return o>i-n?(i+n)/2:(n>=t-r-s&&(t=n+r+s),i<=t+r+s&&(t=i-r-s),t)}function Ot(t){const e=t.options,o=e.arrowHeads&&e.arrowHeads.start,n=e.arrowHeads&&e.arrowHeads.end;return{startOpts:o,endOpts:n,startAdjust:Rt(t,o),endAdjust:Rt(t,n)}}function Rt(t,e){if(!e||!e.display)return 0;const{length:o,width:n}=e,i=t.options.borderWidth/2,r={x:o,y:n+i},s={x:0,y:i};return Math.abs(bt(0,r,s))}function It(t,e,o,n){if(!n||!n.display)return;const{length:i,width:r,fill:s,backgroundColor:a,borderColor:d}=n,l=Math.abs(e-i)+o;t.beginPath(),_(t,n),X(t,n),t.moveTo(l,-r),t.lineTo(e+o,0),t.lineTo(l,r),!0===s?(t.fillStyle=a||d,t.closePath(),t.fill(),t.shadowColor="transparent"):t.shadowColor=n.borderShadowColor,t.stroke()}function Yt(t,{x:e,y:o},{angle:n,adjust:i},r){r&&r.display&&(t.save(),t.translate(e,o),t.rotate(n),It(t,0,-i,r),t.restore())}Ct.defaults={adjustScaleRange:!0,arrowHeads:{display:!1,end:Object.assign({},kt),fill:!1,length:12,start:Object.assign({},kt),width:6},borderDash:[],borderDashOffset:0,borderShadowColor:"transparent",borderWidth:2,curve:!1,controlPoint:{y:"-50%"},display:!0,endValue:void 0,init:void 0,hitTolerance:0,label:{backgroundColor:"rgba(0,0,0,0.8)",backgroundShadowColor:"transparent",borderCapStyle:"butt",borderColor:"black",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderRadius:6,borderShadowColor:"transparent",borderWidth:0,callout:Object.assign({},yt.defaults.callout),color:"#fff",content:null,display:!1,drawTime:void 0,font:{family:void 0,lineHeight:void 0,size:void 0,style:void 0,weight:"bold"},height:void 0,hitTolerance:void 0,opacity:void 0,padding:6,position:"center",rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,textAlign:"center",textStrokeColor:void 0,textStrokeWidth:0,width:void 0,xAdjust:0,yAdjust:0,z:void 0},scaleID:void 0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,value:void 0,xMax:void 0,xMin:void 0,xScaleID:void 0,yMax:void 0,yMin:void 0,yScaleID:void 0,z:0},Ct.descriptors={arrowHeads:{start:{_fallback:!0},end:{_fallback:!0},_fallback:!0}},Ct.defaultRoutes={borderColor:"color"};class Xt extends t.Element{inRange(t,o,n,i){const s=this.options.rotation,d=(this.options.borderWidth+this.options.hitTolerance)/2;if("x"!==n&&"y"!==n)return function(t,o,n,i){const{width:r,height:s,centerX:a,centerY:d}=o,l=r/2,c=s/2;if(l<=0||c<=0)return!1;const h=e.toRadians(n||0),u=Math.cos(h),f=Math.sin(h),x=Math.pow(u*(t.x-a)+f*(t.y-d),2),y=Math.pow(f*(t.x-a)-u*(t.y-d),2);return x/Math.pow(l+i,2)+y/Math.pow(c+i,2)<=1.0001}({x:t,y:o},this.getProps(["width","height","centerX","centerY"],i),s,d);const{x:l,y:c,x2:h,y2:u}=this.getProps(["x","y","x2","y2"],i),f="y"===n?{start:c,end:u}:{start:l,end:h},x=r({x:t,y:o},this.getCenterPoint(i),e.toRadians(-s));return x[n]>=f.start-d-a&&x[n]<=f.end+d+a}getCenterPoint(t){return f(this,t)}draw(t){const{width:o,height:n,centerX:i,centerY:r,options:s}=this;t.save(),Y(t,this.getCenterPoint(),s.rotation),_(t,this.options),t.beginPath(),t.fillStyle=s.backgroundColor;const a=X(t,s);t.ellipse(i,r,n/2,o/2,e.PI/2,0,2*e.PI),t.fill(),a&&(t.shadowColor=s.borderShadowColor,t.stroke()),t.restore()}get label(){return this.elements&&this.elements[0]}resolveElementProperties(t,e){return G(t,e)}}Xt.id="ellipseAnnotation",Xt.defaults={adjustScaleRange:!0,backgroundShadowColor:"transparent",borderDash:[],borderDashOffset:0,borderShadowColor:"transparent",borderWidth:1,display:!0,hitTolerance:0,init:void 0,label:Object.assign({},ft.defaults.label),rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,xMax:void 0,xMin:void 0,xScaleID:void 0,yMax:void 0,yMin:void 0,yScaleID:void 0,z:0},Xt.defaultRoutes={borderColor:"color",backgroundColor:"color"},Xt.descriptors={label:{_fallback:!0}};class _t extends t.Element{inRange(t,e,o,n){const{x:i,y:r,x2:s,y2:a,width:d}=this.getProps(["x","y","x2","y2","width"],n),c=(this.options.borderWidth+this.options.hitTolerance)/2;if("x"!==o&&"y"!==o)return function(t,e,o,n){return!(!t||!e||o<=0)&&Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2)<=Math.pow(o+n,2)}({x:t,y:e},this.getCenterPoint(n),d/2,c);return l("y"===o?{start:r,end:a,value:e}:{start:i,end:s,value:t},c)}getCenterPoint(t){return f(this,t)}draw(t){const e=this.options,o=e.borderWidth;if(e.radius<.1)return;t.save(),t.fillStyle=e.backgroundColor,_(t,e);const n=X(t,e);F(t,this,this.centerX,this.centerY),n&&!I(e.pointStyle)&&(t.shadowColor=e.borderShadowColor,t.stroke()),t.restore(),e.borderWidth=o}resolveElementProperties(t,e){const o=q(t,e);return o.initProperties=A(t,o,e),o}}_t.id="pointAnnotation",_t.defaults={adjustScaleRange:!0,backgroundShadowColor:"transparent",borderDash:[],borderDashOffset:0,borderShadowColor:"transparent",borderWidth:1,display:!0,hitTolerance:0,init:void 0,pointStyle:"circle",radius:10,rotation:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,xAdjust:0,xMax:void 0,xMin:void 0,xScaleID:void 0,xValue:void 0,yAdjust:0,yMax:void 0,yMin:void 0,yScaleID:void 0,yValue:void 0,z:0},_t.defaultRoutes={borderColor:"color",backgroundColor:"color"};class Et extends t.Element{inRange(t,o,n,i){if("x"!==n&&"y"!==n)return this.options.radius>=.1&&this.elements.length>1&&function(t,e,o,n){let i=!1,r=t[t.length-1].getProps(["bX","bY"],n);for(const s of t){const t=s.getProps(["bX","bY"],n);t.bY>o!=r.bY>o&&e<(r.bX-t.bX)*(o-t.bY)/(r.bY-t.bY)+t.bX&&(i=!i),r=t}return i}(this.elements,t,o,i);const s=r({x:t,y:o},this.getCenterPoint(i),e.toRadians(-this.options.rotation)),a=this.elements.map((t=>"y"===n?t.bY:t.bX)),d=Math.min(...a),l=Math.max(...a);return s[n]>=d&&s[n]<=l}getCenterPoint(t){return f(this,t)}draw(t){const{elements:e,options:o}=this;t.save(),t.beginPath(),t.fillStyle=o.backgroundColor,_(t,o);const n=X(t,o);let i=!0;for(const o of e)i?(t.moveTo(o.x,o.y),i=!1):t.lineTo(o.x,o.y);t.closePath(),t.fill(),n&&(t.shadowColor=o.borderShadowColor,t.stroke()),t.restore()}resolveElementProperties(t,o){const n=q(t,o),{sides:i,rotation:r}=o,s=[],a=2*e.PI/i;let d=r*e.RAD_PER_DEG;for(let e=0;e{t.defaults.describe(`elements.${zt[e].id}`,{_fallback:"plugins.annotation.common"})}));const Ft={update:Object.assign},Ht=ot.concat(st),Nt=(t,o)=>e.isObject(o)?Qt(t,o):t,Lt=t=>"color"===t||"font"===t;function Vt(t="line"){return zt[t]?t:(console.warn(`Unknown annotation type: '${t}', defaulting to 'line'`),"line")}function Bt(o,n,i,r){const s=function(e,o,n){if("reset"===n||"none"===n||"resize"===n)return Ft;return new t.Animations(e,o)}(o,i.animations,r),a=n.annotations,d=function(t,e){const o=e.length,n=t.length;if(no&&t.splice(o,n-o);return t}(n.elements,a);for(let t=0;tNt(t,r))):n[i]=Nt(s,r)}return n}function Gt(t,e,o,n){return e.$context||(e.$context=Object.assign(Object.create(t.getContext()),{element:e,get elements(){return o.filter((t=>t&&t.options))},id:n.id,type:"annotation"}))}const Kt=new Map,Zt=t=>"doughnutLabel"!==t.type,te=ot.concat(st);var ee={id:"annotation",version:"3.1.0",beforeRegister(){!function(t,e,o,n=!0){const i=o.split(".");let r=0;for(const a of e.split(".")){const d=i[r++];if(parseInt(a,10){const o=r[t];e.isObject(o)&&(o.id=t,i.push(o))})):e.isArray(r)&&i.push(...r),function(t,e){for(const o of t)ct(o,e)}(i.filter(Zt),t.scales)},afterDataLimits(t,e){const o=Kt.get(t);dt(t,e.scale,o.annotations.filter(Zt).filter((t=>t.display&&t.adjustScaleRange)))},afterUpdate(t,o,n){const i=Kt.get(t);!function(t,o,n){o.listened=T(n,ot,o.listeners),o.moveListened=!1,et.forEach((t=>{e.isFunction(n[t])&&(o.moveListened=!0)})),o.listened&&o.moveListened||o.annotations.forEach((t=>{!o.listened&&e.isFunction(t.click)&&(o.listened=!0),o.moveListened||et.forEach((n=>{e.isFunction(t[n])&&(o.listened=!0,o.moveListened=!0)}))}))}(0,i,n),Bt(t,i,n,o.mode),i.visibleElements=i.elements.filter((t=>!t.skip&&t.options.display)),function(t,o,n){const i=o.visibleElements;o.hooked=T(n,st,o.hooks),o.hooked||i.forEach((t=>{o.hooked||st.forEach((n=>{e.isFunction(t.options[n])&&(o.hooked=!0)}))}))}(0,i,n)},beforeDatasetsDraw(t,e,o){oe(t,"beforeDatasetsDraw",o.clip)},afterDatasetsDraw(t,e,o){oe(t,"afterDatasetsDraw",o.clip)},beforeDatasetDraw(t,e,o){oe(t,e.index,o.clip)},beforeDraw(t,e,o){oe(t,"beforeDraw",o.clip)},afterDraw(t,e,o){oe(t,"afterDraw",o.clip)},beforeEvent(t,e,o){nt(Kt.get(t),e.event,o)&&(e.changed=!0)},afterDestroy(t){Kt.delete(t)},getAnnotations(t){const e=Kt.get(t);return e?e.elements:[]},_getAnnotationElementsAtEventForMode:(t,e,o)=>n(t,e,o),defaults:{animations:{numbers:{properties:["x","y","x2","y2","width","height","centerX","centerY","pointX","pointY","radius"],type:"number"},colors:{properties:["backgroundColor","borderColor"],type:"color"}},clip:!0,interaction:{mode:void 0,axis:void 0,intersect:void 0},common:{drawTime:"afterDatasetsDraw",init:!1,label:{}}},descriptors:{_indexable:!1,_scriptable:t=>!te.includes(t)&&"init"!==t,annotations:{_allKeys:!1,_fallback:(t,e)=>`elements.${zt[Vt(e.type)].id}`},interaction:{_fallback:!0},common:{label:{_indexable:Lt,_fallback:!0},_indexable:Lt}},additionalOptionScopes:[""]};function oe(t,o,n){const{ctx:i,chartArea:r}=t,s=Kt.get(t);n&&e.clipArea(i,r);const a=function(t,e){const o=[];for(const n of t)if(n.options.drawTime===e&&o.push({element:n,main:!0}),n.elements&&n.elements.length)for(const t of n.elements)t.options.display&&t.options.drawTime===e&&o.push({element:t});return o}(s.visibleElements,o).sort(((t,e)=>t.element.options.z-e.element.options.z));for(const t of a)ne(i,r,s,t);n&&e.unclipArea(i)}function ne(t,e,o,n){const i=n.element;n.main?(at(o,i,"beforeDraw"),i.draw(t,e),at(o,i,"afterDraw")):i.draw(t,e)}return t.Chart.register(ee),ee})); \ No newline at end of file diff --git a/js/chartjs-scale-timestack.min.js b/js/chartjs-scale-timestack.min.js index 0c384fa..4999b10 100644 --- a/js/chartjs-scale-timestack.min.js +++ b/js/chartjs-scale-timestack.min.js @@ -1 +1 @@ -var _timestack=(()=>{var Z=Object.create;var v=Object.defineProperty;var tt=Object.getOwnPropertyDescriptor;var et=Object.getOwnPropertyNames;var it=Object.getPrototypeOf,nt=Object.prototype.hasOwnProperty;var Y=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),ot=(r,e)=>{for(var t in e)v(r,t,{get:e[t],enumerable:!0})},N=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of et(e))!nt.call(r,n)&&n!==t&&v(r,n,{get:()=>e[n],enumerable:!(i=tt(e,n))||i.enumerable});return r};var R=(r,e,t)=>(t=r!=null?Z(it(r)):{},N(e||!r||!r.__esModule?v(t,"default",{value:r,enumerable:!0}):t,r)),st=r=>N(v({},"__esModule",{value:!0}),r);var z=Y((lt,P)=>{P.exports=Chart});var E=Y((_t,$)=>{$.exports=luxon});var rt={};ot(rt,{DEF_TICK_GENERATORS:()=>G,DEF_TOOLTIP_FORMAT:()=>q,DaysTickGenerator:()=>D,HM:()=>h,HMS:()=>S,MD:()=>u,MDAY:()=>F,MON:()=>b,TickGenerator:()=>j,TimestackScale:()=>x,YEAR:()=>y,YM:()=>M,YMD:()=>f,YearsTickGenerator:()=>g});var K=R(z());var V=R(z()),U=R(E());var A=R(E());var C=R(E()),B=new Map,H=new Map;function w(r,e,t){let i=[{},0];for(let n of e){if(!n.isValid)throw"invalid datetime";let o=n.toLocaleString(t),s=r.measureText(o).width;s>i[1]&&(i=[n,s,o])}return i[0]}function at(r,e,t,i){let n=`${e.month}/${e.weekday}`,o=B.get(r);if(o)return o[n];let s={year:2024,month:12,day:22,hour:23,minute:59,second:59},m=C.DateTime.fromObject(s,i);function*l(I){for(let k=1;k<=12;k++)yield I.set({month:k})}let c=w(t,l(m),{month:"short"}),a=w(t,l(m),{month:"long"});function*d(I){for(let k=22;k<29;k++)yield I.set({day:k})}let p=w(t,d(c),{weekday:"short"}),T=w(t,d(c),{weekday:"long"}),O=w(t,d(c),{weekday:"narrow"}),Q=w(t,d(a),{weekday:"short"}),W=w(t,d(a),{weekday:"long"}),X=w(t,d(a),{weekday:"narrow"}),J=Object.fromEntries(Object.entries({"undefined/undefined":m,"undefined/short":p,"undefined/long":T,"undefined/narrow":O,"numeric/undefined":m,"numeric/short":p,"numeric/long":T,"numeric/narrow":O,"2-digit/undefined":m,"2-digit/short":p,"2-digit/long":T,"2-digit/narrow":O,"short/undefined":c,"short/short":p,"short/long":T,"short/narrow":O,"long/undefined":a,"long/short":Q,"long/long":W,"long/narrow":X}).map(([I,k])=>[I,k.toObject()]));return B.set(r,J),J[n]}function L(r,e,t){let i=JSON.stringify(r),n=`${t.locale||C.Settings.defaultLocale}/${e.font}`,o=`${n}/${i}`,s=H.get(o);if(s)return s;let m=at(n,r,e,t),c=C.DateTime.fromObject(m,t).toLocaleString(r),a=e.measureText(c).width;return H.set(o,a),a}var j=class{*seq(e){}constructor(e,t){this.top=e,this.bottom=t}estimate(e,t,i,n=!0){let o=this.top,s=this.bottom,m=L(o.fmt,t,i),l=o.maj_fmt?L(o.maj_fmt,t,i):0,c={nticks:e/o.size,label_width:Math.max(m,l)},a;if(s){let d=L(s.short_fmt,t,i),p=n&&s.long_fmt?L(s.long_fmt,t,i):0;a={nticks:e/s.size,label_width:Math.max(d,p)}}return{top:c,bottom:a}}format(e,t,i,n){let o=e.toLocaleString(t&&this.top.maj_fmt?this.top.maj_fmt:this.top.fmt);if(!i||!this.bottom)return o;let s=e.toLocaleString(n&&this.bottom.long_fmt?this.bottom.long_fmt:this.bottom.short_fmt);return[o,s]}create(e,t,i){let n=[];for(let{dt:o,is_major:s,with_bottom:m}of this.seq(e))if(!(o=t)break;n.push({value:o.toMillis(),major:s,label:this.format(o,s,m,i(o))})}return n}create_floating(e,t,i){let n=this.bottom,o=n?i(e)&&n.long_fmt?n.long_fmt:n.short_fmt:void 0,s=o?e.toLocaleString(o):"";return{value:e.toMillis(),label:["",t==="left"?"\u2026"+s:s+"\u2026"]}}patch_formats(e){var i,n;function t(o){for(let s of Object.entries(e)){let m=s[0],l=s[1];m==="hour12"&&o.hour&&(o.hour12=l),o[m]&&(o[m]=s[1])}}t(this.top.fmt),this.top.maj_fmt&&t(this.top.maj_fmt),(i=this.bottom)!=null&&i.short_fmt&&t(this.bottom.short_fmt),(n=this.bottom)!=null&&n.long_fmt&&t(this.bottom.long_fmt)}},_=class extends j{constructor(e,t){let{fmt:i,maj_fmt:n,align:o,maj_unit:s,...m}=e;super({fmt:i,maj_fmt:n,size:A.Duration.fromDurationLike(m).toMillis()},t&&{...t,size:A.Duration.fromObject({[t.unit]:1}).toMillis()}),this.step=m,this.bottom_unit=t==null?void 0:t.unit,this.align=o,this.maj_unit=s}*seq(e){let t=e.startOf(this.align);for(;;){let i=this.bottom_unit?t.startOf(this.bottom_unit).equals(t):!1,n=this.maj_unit?t.startOf(this.maj_unit).equals(t):!1;yield{dt:t,is_major:n,with_bottom:i},t=t.plus(this.step)}}},D=class extends j{constructor(e,t){super({...e,size:e.step*86400*1e3},t&&{...t,size:30*86400*1e3}),this.days=e.days,this.maj_days=e.maj_days??[1],this.bottom_days=t?t.days??[1]:[]}*seq(e){let t=e.startOf("month");for(;;){for(let i of this.days){let n=t.set({day:i}),o=this.maj_days.includes(i),s=this.bottom_days.includes(i);yield{dt:n,is_major:o,with_bottom:s}}t=t.plus({month:1})}}},g=class extends j{constructor(e,t){super({...t,size:e*365*86400*1e3}),this.by_years=e}*seq(e){let t=(e.year/this.by_years|0)*this.by_years,i=e.startOf("year").set({year:t});for(;;)yield{dt:i,is_major:!1,with_bottom:!1},i=i.plus({year:this.by_years})}};var S={hour:"numeric",minute:"numeric",second:"numeric"},h={hour:"numeric",minute:"numeric"},F={day:"numeric"},b={month:"short"},y={year:"numeric"},f={year:"numeric",month:"short",day:"numeric"},M={year:"numeric",month:"short"},u={month:"short",day:"numeric"},G=[new _({second:1,align:"second",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:5,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:10,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:30,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:1,align:"minute",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:5,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:10,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:15,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:30,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:1,align:"hour",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:3,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:6,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:12,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({day:1,align:"day",maj_unit:"month",fmt:F},{unit:"month",short_fmt:b,long_fmt:M}),new D({days:[1,5,10,15,20,25],step:5,fmt:F},{short_fmt:b,long_fmt:M}),new D({days:[1,10,20],step:10,fmt:F},{short_fmt:b,long_fmt:M}),new D({days:[1,15],step:15,fmt:F},{short_fmt:b,long_fmt:M}),new _({month:1,align:"month",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new _({month:3,align:"year",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new _({month:6,align:"year",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new g(1,{fmt:y}),new g(5,{fmt:y}),new g(10,{fmt:y}),new g(25,{fmt:y}),new g(50,{fmt:y}),new g(100,{fmt:y}),new g(1e3,{fmt:y})];var q={year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"numeric",second:"numeric"},x=class extends V.Scale{constructor(e){var n,o,s,m,l;super(e);let t=(s=(o=(n=e.chart)==null?void 0:n.config.options)==null?void 0:o.scales)==null?void 0:s[e.id],i=(m=t==null?void 0:t.timestack)!=null&&m.make_tick_generators?(l=t==null?void 0:t.timestack)==null?void 0:l.make_tick_generators():G;if(this._gens=i,t!=null&&t.timestack.format_style)for(let c of i)c.patch_formats(t==null?void 0:t.timestack.format_style)}init(e){this._dt_opts=e.timestack.datetime??{},super.init(e)}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!1);e=isFinite(e)?e:this._dt_now().startOf("day").toMillis(),t=isFinite(t)?t:this._dt_now().endOf("day").toMillis()+1,this.min=Math.min(e,t-1),this.max=Math.max(e+1,t)}_dt_from_ts(e){return U.DateTime.fromMillis(e,this._dt_opts)}_dt_from_object(e){return U.DateTime.fromObject(e,this._dt_opts)}_dt_now(){return U.DateTime.local(this._dt_opts)}_choose_gen(e){let t=this._gens,i=this.options.timestack.max_density,n=this.options.timestack.density,o=this.options.ticks.maxTicksLimit??1/0,s=[];for(let l of t){let{top:c,bottom:a}=l.estimate(e,this.ctx,this._dt_opts),d=Math.max(c.nticks,(a==null?void 0:a.nticks)??0),p=c.nticks*c.label_width/this.width,T=a?a.nticks*a.label_width/this.width:0,O=Math.max(p,T);O<=i&&d<=o&&s.push([l,Math.abs(O-n)])}return s.length?s.reduce((l,c)=>c[1]t:!0}_need_floating_right_tick(e){let t=this.options.timestack.right_floating_tick_thres;return t===!1?!1:e.length?(this.max-e[e.length-1].value)/(this.max-this.min)>t:!0}_build_ticks(){let{min:e,max:t}=this,i=this._choose_gen(t-e);if(!i)return console.warn("Failed to choose the tick generator"),[];let n=this._dt_from_ts(e),o=this._dt_from_ts(t),s=this._dt_now(),m=a=>!a.hasSame(s,"year"),l=i.create(n,o,m);if(!i.bottom)return l;let c=l.filter(a=>Array.isArray(a.label)&&a.label.length>1);if(this._need_floating_left_tick(c)){let a;if(a=i.create_floating(n,"left",m),c.length){let d=c[0],p=this.ctx.measureText(a.label[1]).width,T=(d.value-this.min)*this.width/(this.max-this.min);p*2>T&&(a=void 0)}a&&l.unshift(a)}if(this._need_floating_right_tick(c)){let a;if(a=i.create_floating(o,"right",m),c.length){let d=c[c.length-1],p=this.ctx.measureText(a.label[1]).width,T=(this.max-d.value)*this.width/(this.max-this.min);p*2>T&&(a=void 0)}a&&l.push(a)}return l}buildTicks(){let e;try{e=this._resolveTickFontOptions(0).string}catch{console.warn("failed to resolve the font")}this.ctx.save(),e&&(this.ctx.font=e);let t=this._build_ticks();return this.ctx.restore(),t}getLabelForValue(e){return this._dt_from_ts(e).toLocaleString(this.options.timestack.tooltip_format)}generateTickLabels(e){}getPixelForValue(e){let t=e===null?NaN:(e-this.min)/(this.max-this.min);return this.getPixelForDecimal(t)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return this.min+t*(this.max-this.min)}};x.id="timestack",x.defaults={timestack:{tooltip_format:q,density:.5,max_density:.75,left_floating_tick_thres:.33,right_floating_tick_thres:!1},ticks:{source:"",maxRotation:0,autoSkip:!1}};K.Chart.register(x);return st(rt);})(); +var _timestack=(()=>{var Z=Object.create;var v=Object.defineProperty;var tt=Object.getOwnPropertyDescriptor;var et=Object.getOwnPropertyNames;var it=Object.getPrototypeOf,nt=Object.prototype.hasOwnProperty;var Y=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),ot=(r,e)=>{for(var t in e)v(r,t,{get:e[t],enumerable:!0})},N=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of et(e))!nt.call(r,n)&&n!==t&&v(r,n,{get:()=>e[n],enumerable:!(i=tt(e,n))||i.enumerable});return r};var R=(r,e,t)=>(t=r!=null?Z(it(r)):{},N(e||!r||!r.__esModule?v(t,"default",{value:r,enumerable:!0}):t,r)),st=r=>N(v({},"__esModule",{value:!0}),r);var z=Y((lt,P)=>{P.exports=Chart});var E=Y((_t,$)=>{$.exports=luxon});var rt={};ot(rt,{DEF_TICK_GENERATORS:()=>G,DEF_TOOLTIP_FORMAT:()=>q,DaysTickGenerator:()=>D,HM:()=>h,HMS:()=>S,MD:()=>u,MDAY:()=>F,MON:()=>b,TickGenerator:()=>j,TimestackScale:()=>x,YEAR:()=>y,YM:()=>M,YMD:()=>f,YearsTickGenerator:()=>g});var K=R(z());var V=R(z()),U=R(E());var A=R(E());var C=R(E()),B=new Map,H=new Map;function w(r,e,t){let i=[{},0];for(let n of e){if(!n.isValid)throw"invalid datetime";let o=n.toLocaleString(t),s=r.measureText(o).width;s>i[1]&&(i=[n,s,o])}return i[0]}function at(r,e,t,i){let n=`${e.month}/${e.weekday}`,o=B.get(r);if(o)return o[n];let s={year:2024,month:12,day:22,hour:23,minute:59,second:59},m=C.DateTime.fromObject(s,i);function*l(I){for(let k=1;k<=12;k++)yield I.set({month:k})}let c=w(t,l(m),{month:"short"}),a=w(t,l(m),{month:"long"});function*d(I){for(let k=22;k<29;k++)yield I.set({day:k})}let p=w(t,d(c),{weekday:"short"}),T=w(t,d(c),{weekday:"long"}),O=w(t,d(c),{weekday:"narrow"}),Q=w(t,d(a),{weekday:"short"}),W=w(t,d(a),{weekday:"long"}),X=w(t,d(a),{weekday:"narrow"}),J=Object.fromEntries(Object.entries({"undefined/undefined":m,"undefined/short":p,"undefined/long":T,"undefined/narrow":O,"numeric/undefined":m,"numeric/short":p,"numeric/long":T,"numeric/narrow":O,"2-digit/undefined":m,"2-digit/short":p,"2-digit/long":T,"2-digit/narrow":O,"short/undefined":c,"short/short":p,"short/long":T,"short/narrow":O,"long/undefined":a,"long/short":Q,"long/long":W,"long/narrow":X}).map(([I,k])=>[I,k.toObject()]));return B.set(r,J),J[n]}function L(r,e,t){let i=JSON.stringify(r),n=`${t.locale||C.Settings.defaultLocale}/${e.font}`,o=`${n}/${i}`,s=H.get(o);if(s)return s;let m=at(n,r,e,t),c=C.DateTime.fromObject(m,t).toLocaleString(r),a=e.measureText(c).width;return H.set(o,a),a}var j=class{*seq(e){}constructor(e,t){this.top=e,this.bottom=t}estimate(e,t,i,n=!0){let o=this.top,s=this.bottom,m=L(o.fmt,t,i),l=o.maj_fmt?L(o.maj_fmt,t,i):0,c={nticks:e/o.size,label_width:Math.max(m,l)},a;if(s){let d=L(s.short_fmt,t,i),p=n&&s.long_fmt?L(s.long_fmt,t,i):0;a={nticks:e/s.size,label_width:Math.max(d,p)}}return{top:c,bottom:a}}format(e,t,i,n){let o=e.toLocaleString(t&&this.top.maj_fmt?this.top.maj_fmt:this.top.fmt);if(!i||!this.bottom)return o;let s=e.toLocaleString(n&&this.bottom.long_fmt?this.bottom.long_fmt:this.bottom.short_fmt);return[o,s]}create(e,t,i){let n=[];for(let{dt:o,is_major:s,with_bottom:m}of this.seq(e))if(!(o=t)break;n.push({value:o.toMillis(),major:s,label:this.format(o,s,m,i(o))})}return n}create_floating(e,t,i){let n=this.bottom,o=n?i(e)&&n.long_fmt?n.long_fmt:n.short_fmt:void 0,s=o?e.toLocaleString(o):"";return{value:e.toMillis(),label:["",t==="left"?"\u2026"+s:s+"\u2026"]}}patch_formats(e){var i,n;function t(o){for(let s of Object.entries(e)){let m=s[0],l=s[1];m==="hour12"&&o.hour&&(o.hour12=l),o[m]&&(o[m]=s[1])}}t(this.top.fmt),this.top.maj_fmt&&t(this.top.maj_fmt),(i=this.bottom)!=null&&i.short_fmt&&t(this.bottom.short_fmt),(n=this.bottom)!=null&&n.long_fmt&&t(this.bottom.long_fmt)}},_=class extends j{constructor(e,t){let{fmt:i,maj_fmt:n,align:o,maj_unit:s,...m}=e;super({fmt:i,maj_fmt:n,size:A.Duration.fromDurationLike(m).toMillis()},t&&{...t,size:A.Duration.fromObject({[t.unit]:1}).toMillis()}),this.step=m,this.bottom_unit=t==null?void 0:t.unit,this.align=o,this.maj_unit=s}*seq(e){let t=e.startOf(this.align);for(;;){let i=this.bottom_unit?t.startOf(this.bottom_unit).equals(t):!1,n=this.maj_unit?t.startOf(this.maj_unit).equals(t):!1;yield{dt:t,is_major:n,with_bottom:i},t=t.plus(this.step)}}},D=class extends j{constructor(e,t){super({...e,size:e.step*86400*1e3},t&&{...t,size:30*86400*1e3}),this.days=e.days,this.maj_days=e.maj_days??[1],this.bottom_days=t?t.days??[1]:[]}*seq(e){let t=e.startOf("month");for(;;){for(let i of this.days){let n=t.set({day:i}),o=this.maj_days.includes(i),s=this.bottom_days.includes(i);yield{dt:n,is_major:o,with_bottom:s}}t=t.plus({month:1})}}},g=class extends j{constructor(e,t){super({...t,size:e*365*86400*1e3}),this.by_years=e}*seq(e){let t=(e.year/this.by_years|0)*this.by_years,i=e.startOf("year").set({year:t});for(;;)yield{dt:i,is_major:!1,with_bottom:!1},i=i.plus({year:this.by_years})}};var S={hour:"numeric",minute:"numeric",second:"numeric"},h={hour:"numeric",minute:"numeric"},F={day:"numeric"},b={month:"short"},y={year:"numeric"},f={year:"numeric",month:"short",day:"numeric"},M={year:"numeric",month:"short"},u={month:"short",day:"numeric"},G=[new _({second:1,align:"second",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:5,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:10,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({second:30,align:"minute",maj_unit:"minute",fmt:S,maj_fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:1,align:"minute",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:5,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:10,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:15,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({minute:30,align:"hour",maj_unit:"hour",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:1,align:"hour",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:3,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:6,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({hour:12,align:"day",maj_unit:"day",fmt:h},{unit:"day",short_fmt:u,long_fmt:f}),new _({day:1,align:"day",maj_unit:"month",fmt:F},{unit:"month",short_fmt:b,long_fmt:M}),new D({days:[1,5,10,15,20,25],step:5,fmt:F},{short_fmt:b,long_fmt:M}),new D({days:[1,10,20],step:10,fmt:F},{short_fmt:b,long_fmt:M}),new D({days:[1,15],step:15,fmt:F},{short_fmt:b,long_fmt:M}),new _({month:1,align:"month",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new _({month:3,align:"year",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new _({month:6,align:"year",maj_unit:"year",fmt:b},{unit:"year",short_fmt:y}),new g(1,{fmt:y}),new g(5,{fmt:y}),new g(10,{fmt:y}),new g(25,{fmt:y}),new g(50,{fmt:y}),new g(100,{fmt:y}),new g(1e3,{fmt:y})];var q={year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"numeric",second:"numeric"},x=class extends V.Scale{constructor(e){var n,o,s,m,l;super(e);let t=(s=(o=(n=e.chart)==null?void 0:n.config.options)==null?void 0:o.scales)==null?void 0:s[e.id],i=(m=t==null?void 0:t.timestack)!=null&&m.make_tick_generators?(l=t==null?void 0:t.timestack)==null?void 0:l.make_tick_generators():G;if(this._gens=i,t!=null&&t.timestack.format_style)for(let c of i)c.patch_formats(t==null?void 0:t.timestack.format_style)}init(e){this._dt_opts=e.timestack.datetime??{},super.init(e)}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!1);e=isFinite(e)?e:this._dt_now().startOf("day").toMillis(),t=isFinite(t)?t:this._dt_now().endOf("day").toMillis()+1,this.min=Math.min(e,t-1),this.max=Math.max(e+1,t)}_dt_from_ts(e){return U.DateTime.fromMillis(e,this._dt_opts)}_dt_from_object(e){return U.DateTime.fromObject(e,this._dt_opts)}_dt_now(){return U.DateTime.local(this._dt_opts)}_choose_gen(e){let t=this._gens,i=this.options.timestack.max_density,n=this.options.timestack.density,o=this.options.ticks.maxTicksLimit??1/0,s=[];for(let l of t){let{top:c,bottom:a}=l.estimate(e,this.ctx,this._dt_opts),d=Math.max(c.nticks,(a==null?void 0:a.nticks)??0),p=c.nticks*c.label_width/this.width,T=a?a.nticks*a.label_width/this.width:0,O=Math.max(p,T);O<=i&&d<=o&&s.push([l,Math.abs(O-n)])}return s.length?s.reduce((l,c)=>c[1]t:!0}_need_floating_right_tick(e){let t=this.options.timestack.right_floating_tick_thres;return t===!1?!1:e.length?(this.max-e[e.length-1].value)/(this.max-this.min)>t:!0}_build_ticks(){let{min:e,max:t}=this,i=this._choose_gen(t-e);if(!i)return console.warn("Failed to choose the tick generator"),[];let n=this._dt_from_ts(e),o=this._dt_from_ts(t),s=this._dt_now(),m=a=>!a.hasSame(s,"year"),l=i.create(n,o,m);if(!i.bottom)return l;let c=l.filter(a=>Array.isArray(a.label)&&a.label.length>1);if(this._need_floating_left_tick(c)){let a;if(a=i.create_floating(n,"left",m),c.length){let d=c[0],p=this.ctx.measureText(a.label[1]).width,T=(d.value-this.min)*this.width/(this.max-this.min);p*2>T&&(a=void 0)}a&&l.unshift(a)}if(this._need_floating_right_tick(c)){let a;if(a=i.create_floating(o,"right",m),c.length){let d=c[c.length-1],p=this.ctx.measureText(a.label[1]).width,T=(this.max-d.value)*this.width/(this.max-this.min);p*2>T&&(a=void 0)}a&&l.push(a)}return l}buildTicks(){let e;try{e=this._resolveTickFontOptions(0).string}catch{console.warn("failed to resolve the font")}this.ctx.save(),e&&(this.ctx.font=e);let t=this._build_ticks();return this.ctx.restore(),t}getLabelForValue(e){return this._dt_from_ts(e).toLocaleString(this.options.timestack.tooltip_format)}generateTickLabels(e){}getPixelForValue(e){let t=e===null?NaN:(e-this.min)/(this.max-this.min);return this.getPixelForDecimal(t)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return this.min+t*(this.max-this.min)}};x.id="timestack",x.defaults={timestack:{tooltip_format:q,density:.5,max_density:.75,left_floating_tick_thres:.33,right_floating_tick_thres:!1},ticks:{source:"",maxRotation:0,autoSkip:!1}};K.Chart.register(x);return st(rt);})(); diff --git a/js/hammer.js b/js/hammer.js index 99d067d..34a8c86 100644 --- a/js/hammer.js +++ b/js/hammer.js @@ -1,7 +1,7 @@ -/*! Hammer.JS - v2.0.7 - 2016-04-22 - * http://hammerjs.github.io/ - * - * Copyright (c) 2016 Jorik Tangelder; - * Licensed under the MIT license */ -!function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(j(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;dc[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;ce;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches),d=this.targetIds;if(b&(Ea|Fa)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=s(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return o(a.target,i)}),b===Ea)for(e=0;e-1&&d.splice(a,1)};setTimeout(e,cb)}}function U(a){for(var b=a.srcEvent.clientX,c=a.srcEvent.clientY,d=0;d=f&&db>=g)return!0}return!1}function V(a,b){this.manager=a,this.set(b)}function W(a){if(p(a,jb))return jb;var b=p(a,kb),c=p(a,lb);return b&&c?jb:b||c?b?kb:lb:p(a,ib)?ib:hb}function X(){if(!fb)return!1;var b={},c=a.CSS&&a.CSS.supports;return["auto","manipulation","pan-y","pan-x","pan-x pan-y","none"].forEach(function(d){b[d]=c?a.CSS.supports("touch-action",d):!0}),b}function Y(a){this.options=la({},this.defaults,a||{}),this.id=v(),this.manager=null,this.options.enable=l(this.options.enable,!0),this.state=nb,this.simultaneous={},this.requireFail=[]}function Z(a){return a&sb?"cancel":a&qb?"end":a&pb?"move":a&ob?"start":""}function $(a){return a==Ma?"down":a==La?"up":a==Ja?"left":a==Ka?"right":""}function _(a,b){var c=b.manager;return c?c.get(a):a}function aa(){Y.apply(this,arguments)}function ba(){aa.apply(this,arguments),this.pX=null,this.pY=null}function ca(){aa.apply(this,arguments)}function da(){Y.apply(this,arguments),this._timer=null,this._input=null}function ea(){aa.apply(this,arguments)}function fa(){aa.apply(this,arguments)}function ga(){Y.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function ha(a,b){return b=b||{},b.recognizers=l(b.recognizers,ha.defaults.preset),new ia(a,b)}function ia(a,b){this.options=la({},ha.defaults,b||{}),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=a,this.input=y(this),this.touchAction=new V(this,this.options.touchAction),ja(this,!0),g(this.options.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function ja(a,b){var c=a.element;if(c.style){var d;g(a.options.cssProps,function(e,f){d=u(c.style,f),b?(a.oldCssProps[d]=c.style[d],c.style[d]=e):c.style[d]=a.oldCssProps[d]||""}),b||(a.oldCssProps={})}}function ka(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var la,ma=["","webkit","Moz","MS","ms","o"],na=b.createElement("div"),oa="function",pa=Math.round,qa=Math.abs,ra=Date.now;la="function"!=typeof Object.assign?function(a){if(a===d||null===a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),c=1;ch&&(b.push(a),h=b.length-1):e&(Ga|Ha)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Za={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},$a="touchstart",_a="touchstart touchmove touchend touchcancel";i(N,x,{handler:function(a){var b=Za[a.type];if(b===Ea&&(this.started=!0),this.started){var c=O.call(this,a,b);b&(Ga|Ha)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}}});var ab={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},bb="touchstart touchmove touchend touchcancel";i(P,x,{handler:function(a){var b=ab[a.type],c=Q.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}});var cb=2500,db=25;i(R,x,{handler:function(a,b,c){var d=c.pointerType==za,e=c.pointerType==Ba;if(!(e&&c.sourceCapabilities&&c.sourceCapabilities.firesTouchEvents)){if(d)S.call(this,b,c);else if(e&&U.call(this,c))return;this.callback(a,b,c)}},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var eb=u(na.style,"touchAction"),fb=eb!==d,gb="compute",hb="auto",ib="manipulation",jb="none",kb="pan-x",lb="pan-y",mb=X();V.prototype={set:function(a){a==gb&&(a=this.compute()),fb&&this.manager.element.style&&mb[a]&&(this.manager.element.style[eb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){k(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),W(a.join(" "))},preventDefaults:function(a){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=p(d,jb)&&!mb[jb],f=p(d,lb)&&!mb[lb],g=p(d,kb)&&!mb[kb];if(e){var h=1===a.pointers.length,i=a.distance<2,j=a.deltaTime<250;if(h&&i&&j)return}return g&&f?void 0:e||f&&c&Na||g&&c&Oa?this.preventSrc(b):void 0},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var nb=1,ob=2,pb=4,qb=8,rb=qb,sb=16,tb=32;Y.prototype={defaults:{},set:function(a){return la(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=_(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=_(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=_(a,this),-1===r(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=_(a,this);var b=r(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(b,a)}var c=this,d=this.state;qb>d&&b(c.options.event+Z(d)),b(c.options.event),a.additionalEvent&&b(a.additionalEvent),d>=qb&&b(c.options.event+Z(d))},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=tb)},canEmit:function(){for(var a=0;af?Ja:Ka,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Ia:0>g?La:Ma,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return aa.prototype.attrTest.call(this,a)&&(this.state&ob||!(this.state&ob)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=$(a.direction);b&&(a.additionalEvent=this.options.event+b),this._super.emit.call(this,a)}}),i(ca,aa,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&ob)},emit:function(a){if(1!==a.scale){var b=a.scale<1?"in":"out";a.additionalEvent=this.options.event+b}this._super.emit.call(this,a)}}),i(da,Y,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[hb]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distanceb.time;if(this._input=a,!d||!c||a.eventType&(Ga|Ha)&&!f)this.reset();else if(a.eventType&Ea)this.reset(),this._timer=e(function(){this.state=rb,this.tryEmit()},b.time,this);else if(a.eventType&Ga)return rb;return tb},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===rb&&(a&&a.eventType&Ga?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=ra(),this.manager.emit(this.options.event,this._input)))}}),i(ea,aa,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&ob)}}),i(fa,aa,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Na|Oa,pointers:1},getTouchAction:function(){return ba.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Na|Oa)?b=a.overallVelocity:c&Na?b=a.overallVelocityX:c&Oa&&(b=a.overallVelocityY),this._super.attrTest.call(this,a)&&c&a.offsetDirection&&a.distance>this.options.threshold&&a.maxPointers==this.options.pointers&&qa(b)>this.options.velocity&&a.eventType&Ga},emit:function(a){var b=$(a.offsetDirection);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),i(ga,Y,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ib]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;dc[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;ce;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches),d=this.targetIds;if(b&(Ea|Fa)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=s(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return o(a.target,i)}),b===Ea)for(e=0;e-1&&d.splice(a,1)};setTimeout(e,cb)}}function U(a){for(var b=a.srcEvent.clientX,c=a.srcEvent.clientY,d=0;d=f&&db>=g)return!0}return!1}function V(a,b){this.manager=a,this.set(b)}function W(a){if(p(a,jb))return jb;var b=p(a,kb),c=p(a,lb);return b&&c?jb:b||c?b?kb:lb:p(a,ib)?ib:hb}function X(){if(!fb)return!1;var b={},c=a.CSS&&a.CSS.supports;return["auto","manipulation","pan-y","pan-x","pan-x pan-y","none"].forEach(function(d){b[d]=c?a.CSS.supports("touch-action",d):!0}),b}function Y(a){this.options=la({},this.defaults,a||{}),this.id=v(),this.manager=null,this.options.enable=l(this.options.enable,!0),this.state=nb,this.simultaneous={},this.requireFail=[]}function Z(a){return a&sb?"cancel":a&qb?"end":a&pb?"move":a&ob?"start":""}function $(a){return a==Ma?"down":a==La?"up":a==Ja?"left":a==Ka?"right":""}function _(a,b){var c=b.manager;return c?c.get(a):a}function aa(){Y.apply(this,arguments)}function ba(){aa.apply(this,arguments),this.pX=null,this.pY=null}function ca(){aa.apply(this,arguments)}function da(){Y.apply(this,arguments),this._timer=null,this._input=null}function ea(){aa.apply(this,arguments)}function fa(){aa.apply(this,arguments)}function ga(){Y.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function ha(a,b){return b=b||{},b.recognizers=l(b.recognizers,ha.defaults.preset),new ia(a,b)}function ia(a,b){this.options=la({},ha.defaults,b||{}),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=a,this.input=y(this),this.touchAction=new V(this,this.options.touchAction),ja(this,!0),g(this.options.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function ja(a,b){var c=a.element;if(c.style){var d;g(a.options.cssProps,function(e,f){d=u(c.style,f),b?(a.oldCssProps[d]=c.style[d],c.style[d]=e):c.style[d]=a.oldCssProps[d]||""}),b||(a.oldCssProps={})}}function ka(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var la,ma=["","webkit","Moz","MS","ms","o"],na=b.createElement("div"),oa="function",pa=Math.round,qa=Math.abs,ra=Date.now;la="function"!=typeof Object.assign?function(a){if(a===d||null===a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),c=1;ch&&(b.push(a),h=b.length-1):e&(Ga|Ha)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Za={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},$a="touchstart",_a="touchstart touchmove touchend touchcancel";i(N,x,{handler:function(a){var b=Za[a.type];if(b===Ea&&(this.started=!0),this.started){var c=O.call(this,a,b);b&(Ga|Ha)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}}});var ab={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},bb="touchstart touchmove touchend touchcancel";i(P,x,{handler:function(a){var b=ab[a.type],c=Q.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}});var cb=2500,db=25;i(R,x,{handler:function(a,b,c){var d=c.pointerType==za,e=c.pointerType==Ba;if(!(e&&c.sourceCapabilities&&c.sourceCapabilities.firesTouchEvents)){if(d)S.call(this,b,c);else if(e&&U.call(this,c))return;this.callback(a,b,c)}},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var eb=u(na.style,"touchAction"),fb=eb!==d,gb="compute",hb="auto",ib="manipulation",jb="none",kb="pan-x",lb="pan-y",mb=X();V.prototype={set:function(a){a==gb&&(a=this.compute()),fb&&this.manager.element.style&&mb[a]&&(this.manager.element.style[eb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){k(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),W(a.join(" "))},preventDefaults:function(a){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=p(d,jb)&&!mb[jb],f=p(d,lb)&&!mb[lb],g=p(d,kb)&&!mb[kb];if(e){var h=1===a.pointers.length,i=a.distance<2,j=a.deltaTime<250;if(h&&i&&j)return}return g&&f?void 0:e||f&&c&Na||g&&c&Oa?this.preventSrc(b):void 0},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var nb=1,ob=2,pb=4,qb=8,rb=qb,sb=16,tb=32;Y.prototype={defaults:{},set:function(a){return la(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=_(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=_(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=_(a,this),-1===r(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=_(a,this);var b=r(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(b,a)}var c=this,d=this.state;qb>d&&b(c.options.event+Z(d)),b(c.options.event),a.additionalEvent&&b(a.additionalEvent),d>=qb&&b(c.options.event+Z(d))},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=tb)},canEmit:function(){for(var a=0;af?Ja:Ka,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Ia:0>g?La:Ma,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return aa.prototype.attrTest.call(this,a)&&(this.state&ob||!(this.state&ob)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=$(a.direction);b&&(a.additionalEvent=this.options.event+b),this._super.emit.call(this,a)}}),i(ca,aa,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&ob)},emit:function(a){if(1!==a.scale){var b=a.scale<1?"in":"out";a.additionalEvent=this.options.event+b}this._super.emit.call(this,a)}}),i(da,Y,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[hb]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distanceb.time;if(this._input=a,!d||!c||a.eventType&(Ga|Ha)&&!f)this.reset();else if(a.eventType&Ea)this.reset(),this._timer=e(function(){this.state=rb,this.tryEmit()},b.time,this);else if(a.eventType&Ga)return rb;return tb},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===rb&&(a&&a.eventType&Ga?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=ra(),this.manager.emit(this.options.event,this._input)))}}),i(ea,aa,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&ob)}}),i(fa,aa,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Na|Oa,pointers:1},getTouchAction:function(){return ba.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Na|Oa)?b=a.overallVelocity:c&Na?b=a.overallVelocityX:c&Oa&&(b=a.overallVelocityY),this._super.attrTest.call(this,a)&&c&a.offsetDirection&&a.distance>this.options.threshold&&a.maxPointers==this.options.pointers&&qa(b)>this.options.velocity&&a.eventType&Ga},emit:function(a){var b=$(a.offsetDirection);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),i(ga,Y,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ib]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance{o.remove()}),o.addEventListener("shown.bs.modal",e=>{o.focus()}),f.show();const g=o.querySelector(".modal-body");if(c.startsWith("#")){const e=document.querySelector(c);g.innerHTML=e?e.innerHTML:"ERROR: Content not found"}else fetch(c,{method:"GET",headers:{"X-Requested-From-Modal":a.substring(1),"Requested-With-Ajax":"ajax"}}).then(e=>e.text()).then(e=>{g.innerHTML=e,window.dispatchEvent(new CustomEvent("neutralFetchCompleted",{detail:{element:o,url:c}}))}).catch(e=>{g.innerHTML=e.message,window.dispatchEvent(new CustomEvent("neutralFetchError",{detail:{element:o,url:c}}))})}startDynamicModal(),window.addEventListener("neutralFetchCompleted",()=>{startDynamicModal()}); \ No newline at end of file diff --git a/js/popper.min.js b/js/popper.min.js index 2a94a02..3938564 100644 --- a/js/popper.min.js +++ b/js/popper.min.js @@ -1,6 +1,6 @@ -/** - * @popperjs/core v2.11.8 - MIT License - */ - -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){"use strict";function t(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function o(e){return"undefined"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var i=Math.max,a=Math.min,s=Math.round;function f(){var e=navigator.userAgentData;return null!=e&&e.brands&&Array.isArray(e.brands)?e.brands.map((function(e){return e.brand+"/"+e.version})).join(" "):navigator.userAgent}function c(){return!/^((?!chrome|android).)*safari/i.test(f())}function p(e,o,i){void 0===o&&(o=!1),void 0===i&&(i=!1);var a=e.getBoundingClientRect(),f=1,p=1;o&&r(e)&&(f=e.offsetWidth>0&&s(a.width)/e.offsetWidth||1,p=e.offsetHeight>0&&s(a.height)/e.offsetHeight||1);var u=(n(e)?t(e):window).visualViewport,l=!c()&&i,d=(a.left+(l&&u?u.offsetLeft:0))/f,h=(a.top+(l&&u?u.offsetTop:0))/p,m=a.width/f,v=a.height/p;return{width:m,height:v,top:h,right:d+m,bottom:h+v,left:d,x:d,y:h}}function u(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function l(e){return e?(e.nodeName||"").toLowerCase():null}function d(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function h(e){return p(d(e)).left+u(e).scrollLeft}function m(e){return t(e).getComputedStyle(e)}function v(e){var t=m(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function y(e,n,o){void 0===o&&(o=!1);var i,a,f=r(n),c=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),m=d(n),y=p(e,c,o),g={scrollLeft:0,scrollTop:0},b={x:0,y:0};return(f||!f&&!o)&&(("body"!==l(n)||v(m))&&(g=(i=n)!==t(i)&&r(i)?{scrollLeft:(a=i).scrollLeft,scrollTop:a.scrollTop}:u(i)),r(n)?((b=p(n,!0)).x+=n.clientLeft,b.y+=n.clientTop):m&&(b.x=h(m))),{x:y.left+g.scrollLeft-b.x,y:y.top+g.scrollTop-b.y,width:y.width,height:y.height}}function g(e){var t=p(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function b(e){return"html"===l(e)?e:e.assignedSlot||e.parentNode||(o(e)?e.host:null)||d(e)}function x(e){return["html","body","#document"].indexOf(l(e))>=0?e.ownerDocument.body:r(e)&&v(e)?e:x(b(e))}function w(e,n){var r;void 0===n&&(n=[]);var o=x(e),i=o===(null==(r=e.ownerDocument)?void 0:r.body),a=t(o),s=i?[a].concat(a.visualViewport||[],v(o)?o:[]):o,f=n.concat(s);return i?f:f.concat(w(b(s)))}function O(e){return["table","td","th"].indexOf(l(e))>=0}function j(e){return r(e)&&"fixed"!==m(e).position?e.offsetParent:null}function E(e){for(var n=t(e),i=j(e);i&&O(i)&&"static"===m(i).position;)i=j(i);return i&&("html"===l(i)||"body"===l(i)&&"static"===m(i).position)?n:i||function(e){var t=/firefox/i.test(f());if(/Trident/i.test(f())&&r(e)&&"fixed"===m(e).position)return null;var n=b(e);for(o(n)&&(n=n.host);r(n)&&["html","body"].indexOf(l(n))<0;){var i=m(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||n}var D="top",A="bottom",L="right",P="left",M="auto",k=[D,A,L,P],W="start",B="end",H="viewport",T="popper",R=k.reduce((function(e,t){return e.concat([t+"-"+W,t+"-"+B])}),[]),S=[].concat(k,[M]).reduce((function(e,t){return e.concat([t,t+"-"+W,t+"-"+B])}),[]),V=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function q(e){var t=new Map,n=new Set,r=[];function o(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var r=t.get(e);r&&o(r)}})),r.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||o(e)})),r}function C(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&o(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function N(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function I(e,r,o){return r===H?N(function(e,n){var r=t(e),o=d(e),i=r.visualViewport,a=o.clientWidth,s=o.clientHeight,f=0,p=0;if(i){a=i.width,s=i.height;var u=c();(u||!u&&"fixed"===n)&&(f=i.offsetLeft,p=i.offsetTop)}return{width:a,height:s,x:f+h(e),y:p}}(e,o)):n(r)?function(e,t){var n=p(e,!1,"fixed"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(r,o):N(function(e){var t,n=d(e),r=u(e),o=null==(t=e.ownerDocument)?void 0:t.body,a=i(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=i(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),f=-r.scrollLeft+h(e),c=-r.scrollTop;return"rtl"===m(o||n).direction&&(f+=i(n.clientWidth,o?o.clientWidth:0)-a),{width:a,height:s,x:f,y:c}}(d(e)))}function _(e,t,o,s){var f="clippingParents"===t?function(e){var t=w(b(e)),o=["absolute","fixed"].indexOf(m(e).position)>=0&&r(e)?E(e):e;return n(o)?t.filter((function(e){return n(e)&&C(e,o)&&"body"!==l(e)})):[]}(e):[].concat(t),c=[].concat(f,[o]),p=c[0],u=c.reduce((function(t,n){var r=I(e,n,s);return t.top=i(r.top,t.top),t.right=a(r.right,t.right),t.bottom=a(r.bottom,t.bottom),t.left=i(r.left,t.left),t}),I(e,p,s));return u.width=u.right-u.left,u.height=u.bottom-u.top,u.x=u.left,u.y=u.top,u}function F(e){return e.split("-")[0]}function U(e){return e.split("-")[1]}function z(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function X(e){var t,n=e.reference,r=e.element,o=e.placement,i=o?F(o):null,a=o?U(o):null,s=n.x+n.width/2-r.width/2,f=n.y+n.height/2-r.height/2;switch(i){case D:t={x:s,y:n.y-r.height};break;case A:t={x:s,y:n.y+n.height};break;case L:t={x:n.x+n.width,y:f};break;case P:t={x:n.x-r.width,y:f};break;default:t={x:n.x,y:n.y}}var c=i?z(i):null;if(null!=c){var p="y"===c?"height":"width";switch(a){case W:t[c]=t[c]-(n[p]/2-r[p]/2);break;case B:t[c]=t[c]+(n[p]/2-r[p]/2)}}return t}function Y(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function G(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function J(e,t){void 0===t&&(t={});var r=t,o=r.placement,i=void 0===o?e.placement:o,a=r.strategy,s=void 0===a?e.strategy:a,f=r.boundary,c=void 0===f?"clippingParents":f,u=r.rootBoundary,l=void 0===u?H:u,h=r.elementContext,m=void 0===h?T:h,v=r.altBoundary,y=void 0!==v&&v,g=r.padding,b=void 0===g?0:g,x=Y("number"!=typeof b?b:G(b,k)),w=m===T?"reference":T,O=e.rects.popper,j=e.elements[y?w:m],E=_(n(j)?j:j.contextElement||d(e.elements.popper),c,l,s),P=p(e.elements.reference),M=X({reference:P,element:O,strategy:"absolute",placement:i}),W=N(Object.assign({},O,M)),B=m===T?W:P,R={top:E.top-B.top+x.top,bottom:B.bottom-E.bottom+x.bottom,left:E.left-B.left+x.left,right:B.right-E.right+x.right},S=e.modifiersData.offset;if(m===T&&S){var V=S[i];Object.keys(R).forEach((function(e){var t=[L,A].indexOf(e)>=0?1:-1,n=[D,A].indexOf(e)>=0?"y":"x";R[e]+=V[n]*t}))}return R}var K={placement:"bottom",modifiers:[],strategy:"absolute"};function Q(){for(var e=arguments.length,t=new Array(e),n=0;n=0?-1:1,i="function"==typeof n?n(Object.assign({},t,{placement:e})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[P,L].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}(n,t.rects,i),e}),{}),s=a[t.placement],f=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=f,t.modifiersData.popperOffsets.y+=c),t.modifiersData[r]=a}},se={left:"right",right:"left",bottom:"top",top:"bottom"};function fe(e){return e.replace(/left|right|bottom|top/g,(function(e){return se[e]}))}var ce={start:"end",end:"start"};function pe(e){return e.replace(/start|end/g,(function(e){return ce[e]}))}function ue(e,t){void 0===t&&(t={});var n=t,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=void 0===f?S:f,p=U(r),u=p?s?R:R.filter((function(e){return U(e)===p})):k,l=u.filter((function(e){return c.indexOf(e)>=0}));0===l.length&&(l=u);var d=l.reduce((function(t,n){return t[n]=J(e,{placement:n,boundary:o,rootBoundary:i,padding:a})[F(n)],t}),{});return Object.keys(d).sort((function(e,t){return d[e]-d[t]}))}var le={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,i=void 0===o||o,a=n.altAxis,s=void 0===a||a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.flipVariations,h=void 0===d||d,m=n.allowedAutoPlacements,v=t.options.placement,y=F(v),g=f||(y===v||!h?[fe(v)]:function(e){if(F(e)===M)return[];var t=fe(e);return[pe(e),t,pe(t)]}(v)),b=[v].concat(g).reduce((function(e,n){return e.concat(F(n)===M?ue(t,{placement:n,boundary:p,rootBoundary:u,padding:c,flipVariations:h,allowedAutoPlacements:m}):n)}),[]),x=t.rects.reference,w=t.rects.popper,O=new Map,j=!0,E=b[0],k=0;k=0,S=R?"width":"height",V=J(t,{placement:B,boundary:p,rootBoundary:u,altBoundary:l,padding:c}),q=R?T?L:P:T?A:D;x[S]>w[S]&&(q=fe(q));var C=fe(q),N=[];if(i&&N.push(V[H]<=0),s&&N.push(V[q]<=0,V[C]<=0),N.every((function(e){return e}))){E=B,j=!1;break}O.set(B,N)}if(j)for(var I=function(e){var t=b.find((function(t){var n=O.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return E=t,"break"},_=h?3:1;_>0;_--){if("break"===I(_))break}t.placement!==E&&(t.modifiersData[r]._skip=!0,t.placement=E,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function de(e,t,n){return i(e,a(t,n))}var he={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=void 0===o||o,f=n.altAxis,c=void 0!==f&&f,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.padding,h=n.tether,m=void 0===h||h,v=n.tetherOffset,y=void 0===v?0:v,b=J(t,{boundary:p,rootBoundary:u,padding:d,altBoundary:l}),x=F(t.placement),w=U(t.placement),O=!w,j=z(x),M="x"===j?"y":"x",k=t.modifiersData.popperOffsets,B=t.rects.reference,H=t.rects.popper,T="function"==typeof y?y(Object.assign({},t.rects,{placement:t.placement})):y,R="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,V={x:0,y:0};if(k){if(s){var q,C="y"===j?D:P,N="y"===j?A:L,I="y"===j?"height":"width",_=k[j],X=_+b[C],Y=_-b[N],G=m?-H[I]/2:0,K=w===W?B[I]:H[I],Q=w===W?-H[I]:-B[I],Z=t.elements.arrow,$=m&&Z?g(Z):{width:0,height:0},ee=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},te=ee[C],ne=ee[N],re=de(0,B[I],$[I]),oe=O?B[I]/2-G-re-te-R.mainAxis:K-re-te-R.mainAxis,ie=O?-B[I]/2+G+re+ne+R.mainAxis:Q+re+ne+R.mainAxis,ae=t.elements.arrow&&E(t.elements.arrow),se=ae?"y"===j?ae.clientTop||0:ae.clientLeft||0:0,fe=null!=(q=null==S?void 0:S[j])?q:0,ce=_+ie-fe,pe=de(m?a(X,_+oe-fe-se):X,_,m?i(Y,ce):Y);k[j]=pe,V[j]=pe-_}if(c){var ue,le="x"===j?D:P,he="x"===j?A:L,me=k[M],ve="y"===M?"height":"width",ye=me+b[le],ge=me-b[he],be=-1!==[D,P].indexOf(x),xe=null!=(ue=null==S?void 0:S[M])?ue:0,we=be?ye:me-B[ve]-H[ve]-xe+R.altAxis,Oe=be?me+B[ve]+H[ve]-xe-R.altAxis:ge,je=m&&be?function(e,t,n){var r=de(e,t,n);return r>n?n:r}(we,me,Oe):de(m?we:ye,me,m?Oe:ge);k[M]=je,V[M]=je-me}t.modifiersData[r]=V}},requiresIfExists:["offset"]};var me={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,r=e.name,o=e.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=F(n.placement),f=z(s),c=[P,L].indexOf(s)>=0?"height":"width";if(i&&a){var p=function(e,t){return Y("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:G(e,k))}(o.padding,n),u=g(i),l="y"===f?D:P,d="y"===f?A:L,h=n.rects.reference[c]+n.rects.reference[f]-a[f]-n.rects.popper[c],m=a[f]-n.rects.reference[f],v=E(i),y=v?"y"===f?v.clientHeight||0:v.clientWidth||0:0,b=h/2-m/2,x=p[l],w=y-u[c]-p[d],O=y/2-u[c]/2+b,j=de(x,O,w),M=f;n.modifiersData[r]=((t={})[M]=j,t.centerOffset=j-O,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?"[data-popper-arrow]":n;null!=r&&("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&C(t.elements.popper,r)&&(t.elements.arrow=r)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ve(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ye(e){return[D,L,A,P].some((function(t){return e[t]>=0}))}var ge={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,i=t.modifiersData.preventOverflow,a=J(t,{elementContext:"reference"}),s=J(t,{altBoundary:!0}),f=ve(a,r),c=ve(s,o,i),p=ye(f),u=ye(c);t.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":u})}},be=Z({defaultModifiers:[ee,te,oe,ie]}),xe=[ee,te,oe,ie,ae,le,he,me,ge],we=Z({defaultModifiers:xe});e.applyStyles=ie,e.arrow=me,e.computeStyles=oe,e.createPopper=we,e.createPopperLite=be,e.defaultModifiers=xe,e.detectOverflow=J,e.eventListeners=ee,e.flip=le,e.hide=ge,e.offset=ae,e.popperGenerator=Z,e.popperOffsets=te,e.preventOverflow=he,Object.defineProperty(e,"__esModule",{value:!0})})); -//# sourceMappingURL=popper.min.js.map +/** + * @popperjs/core v2.11.8 - MIT License + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){"use strict";function t(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function o(e){return"undefined"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var i=Math.max,a=Math.min,s=Math.round;function f(){var e=navigator.userAgentData;return null!=e&&e.brands&&Array.isArray(e.brands)?e.brands.map((function(e){return e.brand+"/"+e.version})).join(" "):navigator.userAgent}function c(){return!/^((?!chrome|android).)*safari/i.test(f())}function p(e,o,i){void 0===o&&(o=!1),void 0===i&&(i=!1);var a=e.getBoundingClientRect(),f=1,p=1;o&&r(e)&&(f=e.offsetWidth>0&&s(a.width)/e.offsetWidth||1,p=e.offsetHeight>0&&s(a.height)/e.offsetHeight||1);var u=(n(e)?t(e):window).visualViewport,l=!c()&&i,d=(a.left+(l&&u?u.offsetLeft:0))/f,h=(a.top+(l&&u?u.offsetTop:0))/p,m=a.width/f,v=a.height/p;return{width:m,height:v,top:h,right:d+m,bottom:h+v,left:d,x:d,y:h}}function u(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function l(e){return e?(e.nodeName||"").toLowerCase():null}function d(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function h(e){return p(d(e)).left+u(e).scrollLeft}function m(e){return t(e).getComputedStyle(e)}function v(e){var t=m(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function y(e,n,o){void 0===o&&(o=!1);var i,a,f=r(n),c=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),m=d(n),y=p(e,c,o),g={scrollLeft:0,scrollTop:0},b={x:0,y:0};return(f||!f&&!o)&&(("body"!==l(n)||v(m))&&(g=(i=n)!==t(i)&&r(i)?{scrollLeft:(a=i).scrollLeft,scrollTop:a.scrollTop}:u(i)),r(n)?((b=p(n,!0)).x+=n.clientLeft,b.y+=n.clientTop):m&&(b.x=h(m))),{x:y.left+g.scrollLeft-b.x,y:y.top+g.scrollTop-b.y,width:y.width,height:y.height}}function g(e){var t=p(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function b(e){return"html"===l(e)?e:e.assignedSlot||e.parentNode||(o(e)?e.host:null)||d(e)}function x(e){return["html","body","#document"].indexOf(l(e))>=0?e.ownerDocument.body:r(e)&&v(e)?e:x(b(e))}function w(e,n){var r;void 0===n&&(n=[]);var o=x(e),i=o===(null==(r=e.ownerDocument)?void 0:r.body),a=t(o),s=i?[a].concat(a.visualViewport||[],v(o)?o:[]):o,f=n.concat(s);return i?f:f.concat(w(b(s)))}function O(e){return["table","td","th"].indexOf(l(e))>=0}function j(e){return r(e)&&"fixed"!==m(e).position?e.offsetParent:null}function E(e){for(var n=t(e),i=j(e);i&&O(i)&&"static"===m(i).position;)i=j(i);return i&&("html"===l(i)||"body"===l(i)&&"static"===m(i).position)?n:i||function(e){var t=/firefox/i.test(f());if(/Trident/i.test(f())&&r(e)&&"fixed"===m(e).position)return null;var n=b(e);for(o(n)&&(n=n.host);r(n)&&["html","body"].indexOf(l(n))<0;){var i=m(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||n}var D="top",A="bottom",L="right",P="left",M="auto",k=[D,A,L,P],W="start",B="end",H="viewport",T="popper",R=k.reduce((function(e,t){return e.concat([t+"-"+W,t+"-"+B])}),[]),S=[].concat(k,[M]).reduce((function(e,t){return e.concat([t,t+"-"+W,t+"-"+B])}),[]),V=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function q(e){var t=new Map,n=new Set,r=[];function o(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var r=t.get(e);r&&o(r)}})),r.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||o(e)})),r}function C(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&o(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function N(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function I(e,r,o){return r===H?N(function(e,n){var r=t(e),o=d(e),i=r.visualViewport,a=o.clientWidth,s=o.clientHeight,f=0,p=0;if(i){a=i.width,s=i.height;var u=c();(u||!u&&"fixed"===n)&&(f=i.offsetLeft,p=i.offsetTop)}return{width:a,height:s,x:f+h(e),y:p}}(e,o)):n(r)?function(e,t){var n=p(e,!1,"fixed"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(r,o):N(function(e){var t,n=d(e),r=u(e),o=null==(t=e.ownerDocument)?void 0:t.body,a=i(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=i(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),f=-r.scrollLeft+h(e),c=-r.scrollTop;return"rtl"===m(o||n).direction&&(f+=i(n.clientWidth,o?o.clientWidth:0)-a),{width:a,height:s,x:f,y:c}}(d(e)))}function _(e,t,o,s){var f="clippingParents"===t?function(e){var t=w(b(e)),o=["absolute","fixed"].indexOf(m(e).position)>=0&&r(e)?E(e):e;return n(o)?t.filter((function(e){return n(e)&&C(e,o)&&"body"!==l(e)})):[]}(e):[].concat(t),c=[].concat(f,[o]),p=c[0],u=c.reduce((function(t,n){var r=I(e,n,s);return t.top=i(r.top,t.top),t.right=a(r.right,t.right),t.bottom=a(r.bottom,t.bottom),t.left=i(r.left,t.left),t}),I(e,p,s));return u.width=u.right-u.left,u.height=u.bottom-u.top,u.x=u.left,u.y=u.top,u}function F(e){return e.split("-")[0]}function U(e){return e.split("-")[1]}function z(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function X(e){var t,n=e.reference,r=e.element,o=e.placement,i=o?F(o):null,a=o?U(o):null,s=n.x+n.width/2-r.width/2,f=n.y+n.height/2-r.height/2;switch(i){case D:t={x:s,y:n.y-r.height};break;case A:t={x:s,y:n.y+n.height};break;case L:t={x:n.x+n.width,y:f};break;case P:t={x:n.x-r.width,y:f};break;default:t={x:n.x,y:n.y}}var c=i?z(i):null;if(null!=c){var p="y"===c?"height":"width";switch(a){case W:t[c]=t[c]-(n[p]/2-r[p]/2);break;case B:t[c]=t[c]+(n[p]/2-r[p]/2)}}return t}function Y(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function G(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function J(e,t){void 0===t&&(t={});var r=t,o=r.placement,i=void 0===o?e.placement:o,a=r.strategy,s=void 0===a?e.strategy:a,f=r.boundary,c=void 0===f?"clippingParents":f,u=r.rootBoundary,l=void 0===u?H:u,h=r.elementContext,m=void 0===h?T:h,v=r.altBoundary,y=void 0!==v&&v,g=r.padding,b=void 0===g?0:g,x=Y("number"!=typeof b?b:G(b,k)),w=m===T?"reference":T,O=e.rects.popper,j=e.elements[y?w:m],E=_(n(j)?j:j.contextElement||d(e.elements.popper),c,l,s),P=p(e.elements.reference),M=X({reference:P,element:O,strategy:"absolute",placement:i}),W=N(Object.assign({},O,M)),B=m===T?W:P,R={top:E.top-B.top+x.top,bottom:B.bottom-E.bottom+x.bottom,left:E.left-B.left+x.left,right:B.right-E.right+x.right},S=e.modifiersData.offset;if(m===T&&S){var V=S[i];Object.keys(R).forEach((function(e){var t=[L,A].indexOf(e)>=0?1:-1,n=[D,A].indexOf(e)>=0?"y":"x";R[e]+=V[n]*t}))}return R}var K={placement:"bottom",modifiers:[],strategy:"absolute"};function Q(){for(var e=arguments.length,t=new Array(e),n=0;n=0?-1:1,i="function"==typeof n?n(Object.assign({},t,{placement:e})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[P,L].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}(n,t.rects,i),e}),{}),s=a[t.placement],f=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=f,t.modifiersData.popperOffsets.y+=c),t.modifiersData[r]=a}},se={left:"right",right:"left",bottom:"top",top:"bottom"};function fe(e){return e.replace(/left|right|bottom|top/g,(function(e){return se[e]}))}var ce={start:"end",end:"start"};function pe(e){return e.replace(/start|end/g,(function(e){return ce[e]}))}function ue(e,t){void 0===t&&(t={});var n=t,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=void 0===f?S:f,p=U(r),u=p?s?R:R.filter((function(e){return U(e)===p})):k,l=u.filter((function(e){return c.indexOf(e)>=0}));0===l.length&&(l=u);var d=l.reduce((function(t,n){return t[n]=J(e,{placement:n,boundary:o,rootBoundary:i,padding:a})[F(n)],t}),{});return Object.keys(d).sort((function(e,t){return d[e]-d[t]}))}var le={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,i=void 0===o||o,a=n.altAxis,s=void 0===a||a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.flipVariations,h=void 0===d||d,m=n.allowedAutoPlacements,v=t.options.placement,y=F(v),g=f||(y===v||!h?[fe(v)]:function(e){if(F(e)===M)return[];var t=fe(e);return[pe(e),t,pe(t)]}(v)),b=[v].concat(g).reduce((function(e,n){return e.concat(F(n)===M?ue(t,{placement:n,boundary:p,rootBoundary:u,padding:c,flipVariations:h,allowedAutoPlacements:m}):n)}),[]),x=t.rects.reference,w=t.rects.popper,O=new Map,j=!0,E=b[0],k=0;k=0,S=R?"width":"height",V=J(t,{placement:B,boundary:p,rootBoundary:u,altBoundary:l,padding:c}),q=R?T?L:P:T?A:D;x[S]>w[S]&&(q=fe(q));var C=fe(q),N=[];if(i&&N.push(V[H]<=0),s&&N.push(V[q]<=0,V[C]<=0),N.every((function(e){return e}))){E=B,j=!1;break}O.set(B,N)}if(j)for(var I=function(e){var t=b.find((function(t){var n=O.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return E=t,"break"},_=h?3:1;_>0;_--){if("break"===I(_))break}t.placement!==E&&(t.modifiersData[r]._skip=!0,t.placement=E,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function de(e,t,n){return i(e,a(t,n))}var he={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=void 0===o||o,f=n.altAxis,c=void 0!==f&&f,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.padding,h=n.tether,m=void 0===h||h,v=n.tetherOffset,y=void 0===v?0:v,b=J(t,{boundary:p,rootBoundary:u,padding:d,altBoundary:l}),x=F(t.placement),w=U(t.placement),O=!w,j=z(x),M="x"===j?"y":"x",k=t.modifiersData.popperOffsets,B=t.rects.reference,H=t.rects.popper,T="function"==typeof y?y(Object.assign({},t.rects,{placement:t.placement})):y,R="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,V={x:0,y:0};if(k){if(s){var q,C="y"===j?D:P,N="y"===j?A:L,I="y"===j?"height":"width",_=k[j],X=_+b[C],Y=_-b[N],G=m?-H[I]/2:0,K=w===W?B[I]:H[I],Q=w===W?-H[I]:-B[I],Z=t.elements.arrow,$=m&&Z?g(Z):{width:0,height:0},ee=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},te=ee[C],ne=ee[N],re=de(0,B[I],$[I]),oe=O?B[I]/2-G-re-te-R.mainAxis:K-re-te-R.mainAxis,ie=O?-B[I]/2+G+re+ne+R.mainAxis:Q+re+ne+R.mainAxis,ae=t.elements.arrow&&E(t.elements.arrow),se=ae?"y"===j?ae.clientTop||0:ae.clientLeft||0:0,fe=null!=(q=null==S?void 0:S[j])?q:0,ce=_+ie-fe,pe=de(m?a(X,_+oe-fe-se):X,_,m?i(Y,ce):Y);k[j]=pe,V[j]=pe-_}if(c){var ue,le="x"===j?D:P,he="x"===j?A:L,me=k[M],ve="y"===M?"height":"width",ye=me+b[le],ge=me-b[he],be=-1!==[D,P].indexOf(x),xe=null!=(ue=null==S?void 0:S[M])?ue:0,we=be?ye:me-B[ve]-H[ve]-xe+R.altAxis,Oe=be?me+B[ve]+H[ve]-xe-R.altAxis:ge,je=m&&be?function(e,t,n){var r=de(e,t,n);return r>n?n:r}(we,me,Oe):de(m?we:ye,me,m?Oe:ge);k[M]=je,V[M]=je-me}t.modifiersData[r]=V}},requiresIfExists:["offset"]};var me={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,r=e.name,o=e.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=F(n.placement),f=z(s),c=[P,L].indexOf(s)>=0?"height":"width";if(i&&a){var p=function(e,t){return Y("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:G(e,k))}(o.padding,n),u=g(i),l="y"===f?D:P,d="y"===f?A:L,h=n.rects.reference[c]+n.rects.reference[f]-a[f]-n.rects.popper[c],m=a[f]-n.rects.reference[f],v=E(i),y=v?"y"===f?v.clientHeight||0:v.clientWidth||0:0,b=h/2-m/2,x=p[l],w=y-u[c]-p[d],O=y/2-u[c]/2+b,j=de(x,O,w),M=f;n.modifiersData[r]=((t={})[M]=j,t.centerOffset=j-O,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?"[data-popper-arrow]":n;null!=r&&("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&C(t.elements.popper,r)&&(t.elements.arrow=r)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ve(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ye(e){return[D,L,A,P].some((function(t){return e[t]>=0}))}var ge={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,i=t.modifiersData.preventOverflow,a=J(t,{elementContext:"reference"}),s=J(t,{altBoundary:!0}),f=ve(a,r),c=ve(s,o,i),p=ye(f),u=ye(c);t.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":u})}},be=Z({defaultModifiers:[ee,te,oe,ie]}),xe=[ee,te,oe,ie,ae,le,he,me,ge],we=Z({defaultModifiers:xe});e.applyStyles=ie,e.arrow=me,e.computeStyles=oe,e.createPopper=we,e.createPopperLite=be,e.defaultModifiers=xe,e.detectOverflow=J,e.eventListeners=ee,e.flip=le,e.hide=ge,e.offset=ae,e.popperGenerator=Z,e.popperOffsets=te,e.preventOverflow=he,Object.defineProperty(e,"__esModule",{value:!0})})); +//# sourceMappingURL=popper.min.js.map diff --git a/js/solar/autoActionFuncs.js b/js/solar/autoActionFuncs.js index e5d9e98..9344151 100644 --- a/js/solar/autoActionFuncs.js +++ b/js/solar/autoActionFuncs.js @@ -1,859 +1,859 @@ -/** - * Editor und Uebersicht fuer die Automatiken. - * - * Der Editor arbeitet auf einem Modell und zeichnet daraus die Oberflaeche - - * nicht umgekehrt. Frueher stand die erste Bedingungszeile als festes HTML im - * PHP und alle weiteren wurden im JavaScript zusammengesetzt; beide Fassungen - * liefen auseinander, und beim Speichern musste der Server aus durchnummerierten - * Feldnamen wieder eine Struktur raten. - * - * Modell: - * autoModel.groups = [ [ {state_id, operator, value}, ... ], ... ] - * eine innere Liste ist ein UND-Block, die aeussere - * verknuepft die Bloecke mit ODER - * autoModel.actions = [ {command_id, params: {parameterID: wert}}, ... ] - * - * Der Geraetekatalog kommt zusammen mit dem Datensatz im selben Dokument - * (siehe ajax/AutoAction.php), es gibt also keine Nachladerei je Zeile. - */ - -let autoModel = null; -let autoDevices = []; -let autoLockouts = []; -let autoRooms = []; - -// --- Katalog-Helfer ------------------------------------------------------ - -function deviceById(id) { - return autoDevices.find(d => d.id === id) || null; -} - -function stateById(id) { - for (const dev of autoDevices) { - const state = dev.states.find(s => s.id === id); - if (state) return { device: dev, state: state }; - } - return null; -} - -function commandById(id) { - for (const dev of autoDevices) { - const cmd = dev.commands.find(c => c.id === id); - if (cmd) return { device: dev, command: cmd }; - } - return null; -} - -function operatorLabel(state, op) { - const treffer = state.operators.find(o => o.value === op); - return treffer ? treffer.label : op; -} - -/** - * Eine neue Zeile beginnt ohne Geraet. - * - * Frueher stand dort das erste Geraet mit seinem ersten Messwert - nach dem - * Geraete-Discovery also "Bad Links: Gesperrt = ", eine Bedingung, die - * niemand gemeint hat und die man erst wegklicken muss. Ohne Vorbelegung - * sagt die Zeile, was zu tun ist, und der Server lehnt eine unvollstaendige - * Bedingung ohnehin ab. - */ -function leereBedingung() { - return { state_id: 0, operator: "=", value: "" }; -} - -function leereAktion() { - return { command_id: 0, params: {} }; -} - -// --- Bausteine fuer die Oberflaeche -------------------------------------- - -function optionListe(select, eintraege, gewaehlt) { - select.innerHTML = ""; - eintraege.forEach(e => select.appendChild(option(e, gewaehlt))); -} - -function option(e, gewaehlt) { - const opt = document.createElement("option"); - opt.value = e.value; - opt.text = e.text; - if (String(e.value) === String(gewaehlt)) opt.selected = true; - return opt; -} - -/** - * Geraeteliste fuer die Auswahl: eine Ueberschrift je Etage, darin nach Raum - * sortiert. - * - * Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll ..."), eine - * Ueberschrift je Raum waeren aber zwanzig Stueck - in einer Liste, die - * fuenfzehn Zeilen hoch ist, saehe man dann mehr Ueberschrift als Geraet, und - * "OG · Bad", "EG · Bad", "UG · Bad" schrieben die Etage immer wieder - * dazu. Deshalb gruppiert nur die Etage; der Raum steht klein an jeder Zeile, - * und weil danach sortiert wird, stehen die Geraete eines Raums trotzdem - * beisammen. - * - * Der Raum steht nur da, wo er von Hand vergeben wurde (Zuordnung ueber das - * Zahnrad in der Karte "Automatismen"). Solange das niemand getan hat, gaebe - * die Etage nichts her - dann bleibt es bei der Ordnung nach Geraeteart. - */ -function geraeteGruppen(geraete) { - if (!geraete.some(d => d.room)) { - return nachGeraeteart(geraete); - } - const etagen = etagenReihenfolge(); - // "Ohne Raum" und "Unbekannter Raum" stehen in keiner Raumliste. Ihr - // indexOf() waere -1 und damit ganz vorne; sie gehoeren aber ans Ende. - const platz = etage => etagen.includes(etage) ? etagen.indexOf(etage) : etagen.length; - const nach = new Map(); - geraete.forEach(d => { - if (!nach.has(d.floorLabel)) nach.set(d.floorLabel, []); - nach.get(d.floorLabel).push(d); - }); - return Array.from(nach.keys()) - .sort((a, b) => platz(a) - platz(b) || a.localeCompare(b, "de")) - .map(etage => { - const liste = nach.get(etage); - liste.sort((a, b) => raumIndex(a.room) - raumIndex(b.room) - || a.type.localeCompare(b.type, "de") - || a.label.localeCompare(b.label, "de")); - return { titel: etage + " (" + liste.length + ")", geraete: liste }; - }); -} - -/** - * Reihenfolge der Etagen-Ueberschriften, wie rooms.php sie notiert - erst OG, - * dann EG, dann UG, dann Aussen. - */ -function etagenReihenfolge() { - const etagen = []; - autoRooms.forEach(r => { if (!etagen.includes(r.floor)) etagen.push(r.floor); }); - return etagen; -} - -/** - * Platz eines Raums in rooms.php - die Sortierung innerhalb einer Etage. - * - * Alles ohne Raum bekommt denselben letzten Platz; dort entscheidet dann die - * Geraeteart, sonst stuenden Jalousie, Steckdose und Thermostat wild - * durcheinander. - */ -function raumIndex(key) { - const i = autoRooms.findIndex(r => r.key === key); - return i < 0 ? autoRooms.length : i; -} - -/** - * Was klein hinter dem Geraetenamen steht. - * - * Unter einer Etagen-Ueberschrift genuegt der Raum. In der Trefferliste einer - * Suche steht keine Ueberschrift darueber, dort muss die Etage mit. Wo kein - * Raum vergeben ist, steht die Geraeteart - die ist immer da und sagt - * wenigstens, was fuer ein Ding das ist. - */ -function zeilenZusatz(d, mitEtage) { - if (!d.roomName) return d.type; - return mitEtage ? d.roomLabel : d.roomName; -} - -/** - * Worueber die Suche laeuft: Name, Raum und Geraeteart in einem Zug. - * - * Der Raum gehoert dazu, seit die Liste nach Etagen gruppiert - "bad" soll - * auch den Handtuchtrockner finden, der im Bad haengt, ohne es im Namen zu - * tragen. Ohne Zuordnung faellt der Raum weg; "Nicht zugeordnet" als - * Suchbegriff waere kein Fund, sondern ein Versehen. - */ -function suchfeld(d) { - return (d.label + " " + (d.roomName ? d.roomLabel : "") + " " + d.type).toLowerCase(); -} - -/** - * Nach Geraeteart gruppieren - der Rueckfall, solange kein Raum vergeben ist. - * - * Gruppen aus einem einzigen Geraet fallen dabei unter "Einzelne" zusammen: - * fuenfzehn Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als - * die flache Liste darunter. - */ -function nachGeraeteart(geraete) { - const nach = new Map(); - geraete.forEach(d => { - if (!nach.has(d.type)) nach.set(d.type, []); - nach.get(d.type).push(d); - }); - const gruppen = []; - const rest = []; - Array.from(nach.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => { - const liste = nach.get(art); - if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste }); - else rest.push(liste[0]); - }); - if (rest.length) { - rest.sort((a, b) => a.label.localeCompare(b.label, "de")); - gruppen.push({ titel: "Einzelne (" + rest.length + ")", geraete: rest }); - } - return gruppen; -} - -/** - * Geraeteauswahl mit Suchfeld. - * - * Bei ueber fuenfzig Geraeten hilft Tippen mehr als Blaettern - und es hilft - * quer zur Gruppierung: "bad" findet das Thermostat im Bad, die Jalousie - * "Bad Links" und alles andere, was einem Bad zugeordnet ist, "jalousie" - * findet alle elf auf einmal. Gesucht wird deshalb ueber Beschriftung, Raum - * und Geraeteart zugleich. - * - * Statt einer Auswahlliste steht ein Knopf in der Zeile; die Liste klappt - * darunter ueber die volle Breite auf. Auf dem Handy ist das mehr Platz als - * ein natives Auswahlfeld hergibt, und das Suchfeld waere darin ohnehin - * nicht unterzubringen. - */ -function geraeteWaehler(geraete, aktuelleId, beiAuswahl) { - const knopf = document.createElement("button"); - knopf.type = "button"; - knopf.className = "form-select text-start auto-geraet"; - const dev = geraete.find(d => d.id === aktuelleId); - knopf.textContent = dev ? dev.label : "Gerät wählen …"; - if (!dev) knopf.classList.add("text-body-secondary"); - knopf.onclick = () => oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl); - return knopf; -} - -/** Die aufgeklappte Liste - es ist immer hoechstens eine offen. */ -function schliesseWaehler() { - const offen = document.querySelector(".geraete-waehler"); - if (offen) offen.remove(); -} - -function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { - const schonOffen = knopf.nextElementSibling - && knopf.nextElementSibling.classList.contains("geraete-waehler"); - schliesseWaehler(); - if (schonOffen) return; // zweiter Klick schliesst wieder - - const panel = document.createElement("div"); - panel.className = "geraete-waehler border rounded p-2 mt-1"; - const suche = document.createElement("input"); - suche.type = "search"; - suche.className = "form-control form-control-sm mb-2"; - suche.placeholder = "Suchen – Name, Raum oder Geräteart"; - const liste = document.createElement("div"); - liste.className = "list-group list-group-flush"; - liste.style.maxHeight = "15rem"; - liste.style.overflowY = "auto"; - panel.appendChild(suche); - panel.appendChild(liste); - // Hinter die ganze Zeile haengen, nicht in sie hinein: in einer - // input-group stuende die Liste sonst als weitere Spalte daneben. - knopf.closest(".input-group").after(panel); - - const eintrag = (d, mitEtage) => { - const zeile = document.createElement("button"); - zeile.type = "button"; - // Der Zusatz steht rechts am Rand statt gleich hinter dem Namen: so - // stehen die Raeume untereinander in einer Spalte und lassen sich - // ueberfliegen, statt bei jeder Zeile woanders anzufangen. - zeile.className = "list-group-item list-group-item-action py-1" - + " d-flex justify-content-between align-items-center gap-2" - + (d.id === aktuelleId ? " active" : ""); - zeile.innerHTML = "" + textSicher(d.label) + "" - + "" - + textSicher(zeilenZusatz(d, mitEtage)) + ""; - zeile.onclick = () => { schliesseWaehler(); beiAuswahl(d); }; - return zeile; - }; - - const zeichne = () => { - const suchtext = suche.value.trim().toLowerCase(); - liste.innerHTML = ""; - const passend = geraete.filter(d => !suchtext || suchfeld(d).includes(suchtext)); - if (!passend.length) { - liste.innerHTML = "
Nichts gefunden.
"; - return; - } - // Beim Suchen die Zwischenueberschriften weglassen: die Treffer stehen - // dann quer ueber alle Etagen. Dafuer traegt jede Zeile die Etage mit - - // "Bad" allein sagt sonst nicht, welches der drei Baeder gemeint ist. - if (suchtext) { - passend.forEach(d => liste.appendChild(eintrag(d, true))); - return; - } - geraeteGruppen(passend).forEach(g => { - const kopf = document.createElement("div"); - kopf.className = "px-2 pt-2 pb-1 small fw-bold text-body-secondary"; - kopf.textContent = g.titel; - liste.appendChild(kopf); - g.geraete.forEach(d => liste.appendChild(eintrag(d, false))); - }); - }; - - suche.oninput = zeichne; - suche.onkeydown = e => { - if (e.key === "Escape") { schliesseWaehler(); knopf.focus(); } - if (e.key === "Enter") { - e.preventDefault(); - const erster = liste.querySelector("button.list-group-item"); - if (erster) erster.click(); - } - }; - zeichne(); - suche.focus(); -} - -// Klick daneben schliesst die Liste - sonst bliebe sie stehen, wenn man es -// sich anders ueberlegt. -document.addEventListener("click", e => { - if (e.target.closest(".geraete-waehler") || e.target.closest(".form-select.text-start")) return; - schliesseWaehler(); -}); - -function textSicher(text) { - return String(text).replace(/&/g, "&").replace(//g, ">"); -} - -/** - * Wertfeld passend zum Datentyp des Messwerts oder Parameters. - * Liefert das fertige Element; der Aufrufer haengt seinen Listener an. - */ -function wertFeld(spec, wert) { - let feld; - if (spec.input === "bool") { - feld = document.createElement("select"); - feld.className = "form-select"; - optionListe(feld, [{ value: "true", text: "JA" }, { value: "false", text: "NEIN" }], - wert === "" ? "true" : wert); - } else if (spec.input === "select") { - // Die Auswahl kommt fertig aus dem Katalog: bei Messwerten steht der - // Klartext auch als Wert, bei Geraeteparametern die Zahl dahinter. - feld = document.createElement("select"); - feld.className = "form-select"; - optionListe(feld, spec.options.map(o => ({ value: o.value, text: o.label })), wert); - } else { - feld = document.createElement("input"); - feld.className = "form-control"; - feld.type = spec.input; - if (spec.min !== undefined && spec.min !== null) feld.min = spec.min; - if (spec.max !== undefined && spec.max !== null) feld.max = spec.max; - feld.value = wert !== "" ? wert - : spec.input === "time" ? "00:00" - : spec.input === "date" ? new Date().toISOString().split("T")[0] - : spec.input === "number" ? "0" : ""; - } - return feld; -} - -/** Standardwert, wenn eine Zeile auf einen anderen Messwert umgestellt wird. */ -function standardWert(spec) { - if (spec.input === "bool") return "true"; - if (spec.input === "select") return spec.options.length ? spec.options[0].value : ""; - if (spec.input === "time") return "00:00"; - if (spec.input === "date") return new Date().toISOString().split("T")[0]; - if (spec.input === "number") return "0"; - return ""; -} - -// --- Ausloeser ----------------------------------------------------------- - -/** - * Zeichnet alle UND-Bloecke. Jeder Block hat sein eigenes "+ Bedingung", denn - * mit einem einzigen Knopf ganz unten kaeme man immer nur an den letzten - * Block heran und koennte eine bestehende Alternative nie mehr ergaenzen. - */ -function renderConditions() { - const ziel = document.getElementById("condBlocks"); - ziel.innerHTML = ""; - - autoModel.groups.forEach((gruppe, gi) => { - if (gi > 0) { - const trenner = document.createElement("div"); - trenner.className = "d-flex align-items-center my-2 text-body-secondary"; - trenner.innerHTML = "
" - + "ODER" - + "
"; - ziel.appendChild(trenner); - } - - const block = document.createElement("div"); - block.className = "border rounded p-2"; - - gruppe.forEach((bed, bi) => { - if (bi > 0) { - const und = document.createElement("div"); - und.className = "small fw-bold text-body-secondary ms-1 my-1"; - und.textContent = "und"; - block.appendChild(und); - } - block.appendChild(conditionRow(gi, bi, bed)); - }); - - const add = document.createElement("button"); - add.type = "button"; - add.className = "btn btn-outline-success btn-sm mt-2"; - add.innerHTML = " Bedingung"; - add.onclick = () => { gruppe.push(leereBedingung()); renderConditions(); }; - block.appendChild(add); - - ziel.appendChild(block); - }); - - renderSummary(); -} - -function conditionRow(gi, bi, bed) { - const zeile = document.createElement("div"); - zeile.className = "input-group auto-zeile"; - - const treffer = stateById(bed.state_id); - const dev = treffer ? treffer.device : null; - const state = treffer ? treffer.state : null; - - // Geraet - zeile.appendChild(geraeteWaehler( - autoDevices.filter(d => d.states.length), dev ? dev.id : 0, - neu => { - bed.state_id = neu.states[0].id; - bed.operator = neu.states[0].operators[0].value; - bed.value = standardWert(neu.states[0]); - renderConditions(); - })); - - // Messwert - nur zeigen, wenn das Geraet mehr als einen hat - if (dev && dev.states.length > 1) { - const messwert = document.createElement("select"); - messwert.className = "form-select auto-messwert"; - optionListe(messwert, dev.states.map(s => ({ - value: s.id, - text: s.name + (s.value ? " (= " + s.value + ")" : "") - })), bed.state_id); - messwert.onchange = () => { - const neu = dev.states.find(s => s.id === Number(messwert.value)); - bed.state_id = neu.id; - bed.operator = neu.operators[0].value; - bed.value = standardWert(neu); - renderConditions(); - }; - zeile.appendChild(messwert); - } - - if (state) { - // Operator. Eine Auswahlliste statt des frueheren Umschalt-Knopfes: der - // zeigte nur seinen aktuellen Zustand, welche Vergleiche es sonst noch - // gibt, fand man erst durch Klicken heraus. - const op = document.createElement("select"); - // Feste, schmale Breite: der Pfeil der Auswahlliste braucht Platz, sonst - // schiebt er das Zeichen aus dem sichtbaren Bereich. - op.className = "form-select flex-grow-0 text-center"; - op.style.flex = "0 0 5.5rem"; - optionListe(op, state.operators.map(o => ({ value: o.value, text: o.label })), bed.operator); - op.onchange = () => { bed.operator = op.value; renderSummary(); }; - zeile.appendChild(op); - - const wert = wertFeld(state, bed.value); - wert.onchange = () => { bed.value = wert.value; renderSummary(); }; - zeile.appendChild(wert); - - if (state.unit) { - const einheit = document.createElement("span"); - einheit.className = "input-group-text"; - einheit.textContent = state.unit; - zeile.appendChild(einheit); - } - } - - const weg = document.createElement("button"); - weg.type = "button"; - weg.className = "btn btn-outline-danger"; - weg.innerHTML = ""; - weg.onclick = () => { - autoModel.groups[gi].splice(bi, 1); - // Ein leerer Block hat keine Bedeutung mehr und verschwindet mit. - if (!autoModel.groups[gi].length) autoModel.groups.splice(gi, 1); - renderConditions(); - }; - zeile.appendChild(weg); - - return zeile; -} - -/** - * Derselbe Satz, den auch die Uebersicht und der Runner benutzen. Er steht - * unter den Bloecken, damit man die Klammerung nicht aus der Optik erschliessen - * muss, sondern schwarz auf weiss liest. - */ -function conditionText() { - const teile = autoModel.groups - .map(g => g.filter(bed => stateById(bed.state_id))) - .filter(g => g.length) - .map(gruppe => { - const text = gruppe.map(bed => { - const treffer = stateById(bed.state_id); - let s = treffer.device.label + ": " + treffer.state.name + " " - + operatorLabel(treffer.state, bed.operator) + " " + bed.value; - if (treffer.state.unit) s += " " + treffer.state.unit; - return s; - }).join(" und "); - return (autoModel.groups.length > 1 && gruppe.length > 1) ? "(" + text + ")" : text; - }); - return teile.join(" oder "); -} - -function renderSummary() { - const text = conditionText(); - document.getElementById("condSummary").innerHTML = text - ? "Löst aus, wenn " + text.replace(/&/g, "&").replace(/." - : "Noch kein Auslöser – die Automatik würde nie starten."; -} - -// --- Aktionen ------------------------------------------------------------ - -function renderActions() { - const ziel = document.getElementById("actionList"); - ziel.innerHTML = ""; - autoModel.actions.forEach((aktion, ai) => ziel.appendChild(actionRow(ai, aktion))); -} - -function actionRow(ai, aktion) { - const zeile = document.createElement("div"); - zeile.className = "input-group mb-2 auto-zeile"; - - const treffer = commandById(aktion.command_id); - const dev = treffer ? treffer.device : null; - const cmd = treffer ? treffer.command : null; - - zeile.appendChild(geraeteWaehler( - autoDevices.filter(d => d.commands.length), dev ? dev.id : 0, - neu => { - aktion.command_id = neu.commands[0].id; - aktion.params = standardParams(neu.commands[0]); - renderActions(); - })); - - if (dev && dev.commands.length > 1) { - const befehl = document.createElement("select"); - befehl.className = "form-select auto-messwert"; - optionListe(befehl, dev.commands.map(c => ({ value: c.id, text: c.name })), aktion.command_id); - befehl.onchange = () => { - const neu = dev.commands.find(c => c.id === Number(befehl.value)); - aktion.command_id = neu.id; - aktion.params = standardParams(neu); - renderActions(); - }; - zeile.appendChild(befehl); - } - - // Je Parameter ein Feld - wie viele es sind, sagt das Geraet. Frueher gab es - // vier feste Felder "Wert 1" bis "Wert 4", die je nach Kommando versteckt - // wurden, ihren Inhalt aber trotzdem mitschickten. - if (cmd) { - cmd.params.forEach(p => { - const beschriftung = document.createElement("span"); - beschriftung.className = "input-group-text"; - beschriftung.textContent = p.name; - zeile.appendChild(beschriftung); - - const wert = wertFeld(p, aktion.params[p.id] !== undefined ? aktion.params[p.id] : standardWert(p)); - wert.onchange = () => { aktion.params[p.id] = wert.value; }; - zeile.appendChild(wert); - }); - } - - const weg = document.createElement("button"); - weg.type = "button"; - weg.className = "btn btn-outline-danger"; - weg.innerHTML = ""; - weg.onclick = () => { autoModel.actions.splice(ai, 1); renderActions(); }; - zeile.appendChild(weg); - - return zeile; -} - -function standardParams(cmd) { - const params = {}; - cmd.params.forEach(p => { params[p.id] = standardWert(p); }); - return params; -} - -// --- Rahmenbedingungen --------------------------------------------------- - -const WOCHENTAGE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; - -function renderWeekdays() { - const ziel = document.getElementById("weekdayGroup"); - ziel.innerHTML = ""; - WOCHENTAGE.forEach((tag, i) => { - const feld = document.createElement("div"); - feld.className = "input-group-text"; - feld.innerHTML = "" - + ""; - feld.querySelector("input").onchange = e => { - if (e.target.checked) autoModel.weekdays |= (1 << i); - else autoModel.weekdays &= ~(1 << i); - }; - ziel.appendChild(feld); - }); -} - -function fuelleFormular() { - const sperre = document.getElementById("lockoutSecs"); - optionListe(sperre, autoLockouts.map(o => ({ value: o.value, text: o.label })), - autoModel.lockout_secs); - document.getElementById("autoName").value = autoModel.name; - // Kennt die Liste die Etage nicht, bleibt eine Auswahlliste sonst leer - // stehen und schickt beim Speichern "" mit. - const etage = document.getElementById("autoFloor"); - etage.value = autoModel.floor; - if (etage.selectedIndex < 0) { - etage.selectedIndex = 0; - autoModel.floor = etage.value; - } - document.getElementById("tSpanFrom").value = autoModel.window_from; - document.getElementById("tSpanTo").value = autoModel.window_to; - document.getElementById("actFerien").checked = autoModel.on_vacation; - document.getElementById("actFeier").checked = autoModel.on_holiday; - document.getElementById("runOnce").checked = autoModel.force_once; - renderWeekdays(); - renderConditions(); - renderActions(); -} - -/** Die Felder, die direkt am Formular haengen, ins Modell zurueckholen. */ -function leseFormular() { - autoModel.name = document.getElementById("autoName").value.trim(); - autoModel.floor = document.getElementById("autoFloor").value; - autoModel.window_from = document.getElementById("tSpanFrom").value; - autoModel.window_to = document.getElementById("tSpanTo").value; - autoModel.on_vacation = document.getElementById("actFerien").checked; - autoModel.on_holiday = document.getElementById("actFeier").checked; - autoModel.force_once = document.getElementById("runOnce").checked; - autoModel.lockout_secs = Number(document.getElementById("lockoutSecs").value); -} - -// --- Modal --------------------------------------------------------------- - -function loadAutomatic() { - const daten = JSON.parse(document.getElementById("autoActionData").textContent); - autoDevices = daten.devices; - autoLockouts = daten.lockouts; - autoRooms = daten.rooms || []; - - const a = daten.automation; - autoModel = { - id: a.id, - name: a.name, - floor: a.floor, - enabled: a.enabled, - window_from: a.window_from, - window_to: a.window_to, - weekdays: a.weekdays, - on_vacation: a.on_vacation, - on_holiday: a.on_holiday, - force_once: a.force_once, - lockout_secs: a.lockout_secs, - groups: [], - actions: a.actions.map(x => ({ command_id: x.command_id, params: Object.assign({}, x.params) })) - }; - - // Die flache Liste aus der Datenbank wieder in Bloecke fassen. Die - // Gruppennummern kommen aufsteigend und dicht, siehe saveAutomation(). - const nachGruppe = new Map(); - a.conditions.forEach(c => { - if (!nachGruppe.has(c.group)) nachGruppe.set(c.group, []); - nachGruppe.get(c.group).push({ state_id: c.state_id, operator: c.operator, value: c.value }); - }); - autoModel.groups = Array.from(nachGruppe.values()); - - // Eine neue Automatik startet mit je einer leeren Zeile, sonst steht man - // vor einem leeren Formular ohne erkennbaren Anfang. - if (!autoModel.groups.length) autoModel.groups = [[leereBedingung()]]; - if (!autoModel.actions.length) autoModel.actions = [leereAktion()]; - - fuelleFormular(); -} - -function openAutoActionModal(params) { - const contentURL = "./ajax/AutoAction.php" + params; - document.getElementById("modal-title").innerHTML = - params.search("id=") > 0 ? "Automatik bearbeiten" : "Neue Automatik anlegen"; - - // Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne - // Zuhoerer stellt sicher, dass nicht auch noch der Handler eines vorher - // geoeffneten Modals mitfeuert. - const alt = document.getElementById("modalSaveBtn"); - const btn = alt.cloneNode(true); - alt.replaceWith(btn); - // Das Raum-Modal versteckt ihn; hier wird er gebraucht. - btn.hidden = false; - btn.addEventListener("click", submitAutoAction); - - // Der Editor braucht mehr Breite als die Ladedialoge, gibt sie aber wieder - // her, damit die anderen Modals unveraendert aussehen. - const dialog = document.querySelector("#modalEV .modal-dialog"); - dialog.classList.add("modal-xl"); - document.getElementById("modalEV").addEventListener("hidden.bs.modal", - () => dialog.classList.remove("modal-xl"), { once: true }); - - const modalBodyElement = document.getElementById("modal-body"); - modalBodyElement.innerHTML = loadingHTML("Wird geladen..."); - modalEV.show(); - - fetch(contentURL, { - method: "GET", - headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" } - }) - .then(response => response.text()) - .then(html => { - modalBodyElement.innerHTML = html; - loadAutomatic(); - document.getElementById("btnAddGroup").addEventListener("click", () => { - autoModel.groups.push([leereBedingung()]); - renderConditions(); - }); - document.getElementById("btnAddActor").addEventListener("click", () => { - autoModel.actions.push(leereAktion()); - renderActions(); - }); - }) - .catch(error => { - modalBodyElement.innerHTML = "Konnte den Editor nicht laden: " + error.message; - }); -} - -function submitAutoAction() { - leseFormular(); - const nutzlast = { - id: autoModel.id, - name: autoModel.name, - floor: autoModel.floor, - enabled: autoModel.enabled, - window_from: autoModel.window_from, - window_to: autoModel.window_to, - weekdays: autoModel.weekdays, - on_vacation: autoModel.on_vacation, - on_holiday: autoModel.on_holiday, - force_once: autoModel.force_once, - lockout_secs: autoModel.lockout_secs, - conditions: [], - actions: autoModel.actions - }; - autoModel.groups.forEach((gruppe, gi) => { - gruppe.forEach(bed => { - nutzlast.conditions.push({ group: gi, state_id: bed.state_id, operator: bed.operator, value: bed.value }); - }); - }); - - automationRequest("save", nutzlast).then(antwort => { - if (antwort.error) { - alert(antwort.error); - return; - } - modalEV.hide(); - refreshAutomations(nutzlast.floor); - }); -} - -// --- Uebersicht ---------------------------------------------------------- - -function automationRequest(action, body) { - return fetch("./ajax/AutoAction.php?action=" + action, { - method: "POST", - headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" }, - body: JSON.stringify(body) - }) - .then(response => response.json()) - .catch(error => ({ error: error.message })); -} - -/** - * Die Tabelle einer Etage neu holen. Ohne Etage alle, die es gibt. - * - * Welche das sind, sagen die Reiter - home.php baut sie aus der Raumtabelle. - * Eine feste Liste hier haette beim Aussengelaende still eine Etage - * ausgelassen: die Tabelle waere leer geblieben, ohne Fehlermeldung. - */ -function refreshAutomations(floor) { - const etagen = floor ? [floor] - : Array.from(document.querySelectorAll("#actions-tabContent .tab-pane")) - .map(pane => pane.id.replace(/^actions-/, "")); - etagen.forEach(etage => { - const ziel = document.getElementById("actions-" + etage + "-list"); - if (!ziel) return; - fetch("./ajax/AutoAction.php?action=list&floor=" + etage, { - method: "GET", - headers: { "Requested-With-Ajax": "ajax" } - }) - .then(response => response.text()) - .then(html => { ziel.innerHTML = html; }) - .catch(() => { ziel.innerHTML = "

Liste konnte nicht geladen werden.

"; }); - }); -} - -/** - * Zuordnung Geraet -> Raum. Eigenes Modal, weil es kein Teil einer einzelnen - * Automatik ist, sondern einmal fuer alle gilt. - */ -function openRoomModal() { - document.getElementById("modal-title").innerHTML = "Geräte den Räumen zuordnen"; - const alt = document.getElementById("modalSaveBtn"); - const btn = alt.cloneNode(true); - alt.replaceWith(btn); - // Das Raum-Modal versteckt ihn; hier wird er gebraucht. - btn.hidden = false; - btn.addEventListener("click", speichereRaeume); - - const dialog = document.querySelector("#modalEV .modal-dialog"); - dialog.classList.add("modal-lg"); - document.getElementById("modalEV").addEventListener("hidden.bs.modal", - () => dialog.classList.remove("modal-lg"), { once: true }); - - const koerper = document.getElementById("modal-body"); - koerper.innerHTML = loadingHTML("Wird geladen..."); - modalEV.show(); - - fetch("./ajax/AutoAction.php?action=rooms", { - method: "GET", headers: { "Requested-With-Ajax": "ajax" } - }) - .then(a => a.text()) - .then(html => { - koerper.innerHTML = html; - const tipps = document.getElementById("btnRoomTipps"); - if (tipps) { - tipps.addEventListener("click", () => { - koerper.querySelectorAll("select[data-tipp]").forEach(s => { - if (!s.value) s.value = s.dataset.tipp; - }); - tipps.disabled = true; - }); - } - }) - .catch(fehler => { koerper.innerHTML = "Konnte die Zuordnung nicht laden: " + fehler.message; }); -} - -function speichereRaeume() { - const rooms = {}; - document.querySelectorAll("#modal-body select[data-actor]").forEach(s => { - rooms[s.dataset.actor] = s.value; - }); - automationRequest("saveRooms", { rooms: rooms }).then(antwort => { - if (antwort.error) alert(antwort.error); - else modalEV.hide(); - }); -} - -function toggleAutomation(id) { - automationRequest("toggle", { id: id }).then(antwort => { - if (antwort.error) alert(antwort.error); - else refreshAutomations(null); - }); -} - -function deleteAutomation(id, name) { - if (!confirm("Automatik \"" + name + "\" wirklich löschen?")) return; - automationRequest("delete", { id: id }).then(antwort => { - if (antwort.error) alert(antwort.error); - else refreshAutomations(null); - }); -} - -document.addEventListener("DOMContentLoaded", () => refreshAutomations(null)); +/** + * Editor und Uebersicht fuer die Automatiken. + * + * Der Editor arbeitet auf einem Modell und zeichnet daraus die Oberflaeche - + * nicht umgekehrt. Frueher stand die erste Bedingungszeile als festes HTML im + * PHP und alle weiteren wurden im JavaScript zusammengesetzt; beide Fassungen + * liefen auseinander, und beim Speichern musste der Server aus durchnummerierten + * Feldnamen wieder eine Struktur raten. + * + * Modell: + * autoModel.groups = [ [ {state_id, operator, value}, ... ], ... ] + * eine innere Liste ist ein UND-Block, die aeussere + * verknuepft die Bloecke mit ODER + * autoModel.actions = [ {command_id, params: {parameterID: wert}}, ... ] + * + * Der Geraetekatalog kommt zusammen mit dem Datensatz im selben Dokument + * (siehe ajax/AutoAction.php), es gibt also keine Nachladerei je Zeile. + */ + +let autoModel = null; +let autoDevices = []; +let autoLockouts = []; +let autoRooms = []; + +// --- Katalog-Helfer ------------------------------------------------------ + +function deviceById(id) { + return autoDevices.find(d => d.id === id) || null; +} + +function stateById(id) { + for (const dev of autoDevices) { + const state = dev.states.find(s => s.id === id); + if (state) return { device: dev, state: state }; + } + return null; +} + +function commandById(id) { + for (const dev of autoDevices) { + const cmd = dev.commands.find(c => c.id === id); + if (cmd) return { device: dev, command: cmd }; + } + return null; +} + +function operatorLabel(state, op) { + const treffer = state.operators.find(o => o.value === op); + return treffer ? treffer.label : op; +} + +/** + * Eine neue Zeile beginnt ohne Geraet. + * + * Frueher stand dort das erste Geraet mit seinem ersten Messwert - nach dem + * Geraete-Discovery also "Bad Links: Gesperrt = ", eine Bedingung, die + * niemand gemeint hat und die man erst wegklicken muss. Ohne Vorbelegung + * sagt die Zeile, was zu tun ist, und der Server lehnt eine unvollstaendige + * Bedingung ohnehin ab. + */ +function leereBedingung() { + return { state_id: 0, operator: "=", value: "" }; +} + +function leereAktion() { + return { command_id: 0, params: {} }; +} + +// --- Bausteine fuer die Oberflaeche -------------------------------------- + +function optionListe(select, eintraege, gewaehlt) { + select.innerHTML = ""; + eintraege.forEach(e => select.appendChild(option(e, gewaehlt))); +} + +function option(e, gewaehlt) { + const opt = document.createElement("option"); + opt.value = e.value; + opt.text = e.text; + if (String(e.value) === String(gewaehlt)) opt.selected = true; + return opt; +} + +/** + * Geraeteliste fuer die Auswahl: eine Ueberschrift je Etage, darin nach Raum + * sortiert. + * + * Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll ..."), eine + * Ueberschrift je Raum waeren aber zwanzig Stueck - in einer Liste, die + * fuenfzehn Zeilen hoch ist, saehe man dann mehr Ueberschrift als Geraet, und + * "OG · Bad", "EG · Bad", "UG · Bad" schrieben die Etage immer wieder + * dazu. Deshalb gruppiert nur die Etage; der Raum steht klein an jeder Zeile, + * und weil danach sortiert wird, stehen die Geraete eines Raums trotzdem + * beisammen. + * + * Der Raum steht nur da, wo er von Hand vergeben wurde (Zuordnung ueber das + * Zahnrad in der Karte "Automatismen"). Solange das niemand getan hat, gaebe + * die Etage nichts her - dann bleibt es bei der Ordnung nach Geraeteart. + */ +function geraeteGruppen(geraete) { + if (!geraete.some(d => d.room)) { + return nachGeraeteart(geraete); + } + const etagen = etagenReihenfolge(); + // "Ohne Raum" und "Unbekannter Raum" stehen in keiner Raumliste. Ihr + // indexOf() waere -1 und damit ganz vorne; sie gehoeren aber ans Ende. + const platz = etage => etagen.includes(etage) ? etagen.indexOf(etage) : etagen.length; + const nach = new Map(); + geraete.forEach(d => { + if (!nach.has(d.floorLabel)) nach.set(d.floorLabel, []); + nach.get(d.floorLabel).push(d); + }); + return Array.from(nach.keys()) + .sort((a, b) => platz(a) - platz(b) || a.localeCompare(b, "de")) + .map(etage => { + const liste = nach.get(etage); + liste.sort((a, b) => raumIndex(a.room) - raumIndex(b.room) + || a.type.localeCompare(b.type, "de") + || a.label.localeCompare(b.label, "de")); + return { titel: etage + " (" + liste.length + ")", geraete: liste }; + }); +} + +/** + * Reihenfolge der Etagen-Ueberschriften, wie rooms.php sie notiert - erst OG, + * dann EG, dann UG, dann Aussen. + */ +function etagenReihenfolge() { + const etagen = []; + autoRooms.forEach(r => { if (!etagen.includes(r.floor)) etagen.push(r.floor); }); + return etagen; +} + +/** + * Platz eines Raums in rooms.php - die Sortierung innerhalb einer Etage. + * + * Alles ohne Raum bekommt denselben letzten Platz; dort entscheidet dann die + * Geraeteart, sonst stuenden Jalousie, Steckdose und Thermostat wild + * durcheinander. + */ +function raumIndex(key) { + const i = autoRooms.findIndex(r => r.key === key); + return i < 0 ? autoRooms.length : i; +} + +/** + * Was klein hinter dem Geraetenamen steht. + * + * Unter einer Etagen-Ueberschrift genuegt der Raum. In der Trefferliste einer + * Suche steht keine Ueberschrift darueber, dort muss die Etage mit. Wo kein + * Raum vergeben ist, steht die Geraeteart - die ist immer da und sagt + * wenigstens, was fuer ein Ding das ist. + */ +function zeilenZusatz(d, mitEtage) { + if (!d.roomName) return d.type; + return mitEtage ? d.roomLabel : d.roomName; +} + +/** + * Worueber die Suche laeuft: Name, Raum und Geraeteart in einem Zug. + * + * Der Raum gehoert dazu, seit die Liste nach Etagen gruppiert - "bad" soll + * auch den Handtuchtrockner finden, der im Bad haengt, ohne es im Namen zu + * tragen. Ohne Zuordnung faellt der Raum weg; "Nicht zugeordnet" als + * Suchbegriff waere kein Fund, sondern ein Versehen. + */ +function suchfeld(d) { + return (d.label + " " + (d.roomName ? d.roomLabel : "") + " " + d.type).toLowerCase(); +} + +/** + * Nach Geraeteart gruppieren - der Rueckfall, solange kein Raum vergeben ist. + * + * Gruppen aus einem einzigen Geraet fallen dabei unter "Einzelne" zusammen: + * fuenfzehn Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als + * die flache Liste darunter. + */ +function nachGeraeteart(geraete) { + const nach = new Map(); + geraete.forEach(d => { + if (!nach.has(d.type)) nach.set(d.type, []); + nach.get(d.type).push(d); + }); + const gruppen = []; + const rest = []; + Array.from(nach.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => { + const liste = nach.get(art); + if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste }); + else rest.push(liste[0]); + }); + if (rest.length) { + rest.sort((a, b) => a.label.localeCompare(b.label, "de")); + gruppen.push({ titel: "Einzelne (" + rest.length + ")", geraete: rest }); + } + return gruppen; +} + +/** + * Geraeteauswahl mit Suchfeld. + * + * Bei ueber fuenfzig Geraeten hilft Tippen mehr als Blaettern - und es hilft + * quer zur Gruppierung: "bad" findet das Thermostat im Bad, die Jalousie + * "Bad Links" und alles andere, was einem Bad zugeordnet ist, "jalousie" + * findet alle elf auf einmal. Gesucht wird deshalb ueber Beschriftung, Raum + * und Geraeteart zugleich. + * + * Statt einer Auswahlliste steht ein Knopf in der Zeile; die Liste klappt + * darunter ueber die volle Breite auf. Auf dem Handy ist das mehr Platz als + * ein natives Auswahlfeld hergibt, und das Suchfeld waere darin ohnehin + * nicht unterzubringen. + */ +function geraeteWaehler(geraete, aktuelleId, beiAuswahl) { + const knopf = document.createElement("button"); + knopf.type = "button"; + knopf.className = "form-select text-start auto-geraet"; + const dev = geraete.find(d => d.id === aktuelleId); + knopf.textContent = dev ? dev.label : "Gerät wählen …"; + if (!dev) knopf.classList.add("text-body-secondary"); + knopf.onclick = () => oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl); + return knopf; +} + +/** Die aufgeklappte Liste - es ist immer hoechstens eine offen. */ +function schliesseWaehler() { + const offen = document.querySelector(".geraete-waehler"); + if (offen) offen.remove(); +} + +function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { + const schonOffen = knopf.nextElementSibling + && knopf.nextElementSibling.classList.contains("geraete-waehler"); + schliesseWaehler(); + if (schonOffen) return; // zweiter Klick schliesst wieder + + const panel = document.createElement("div"); + panel.className = "geraete-waehler border rounded p-2 mt-1"; + const suche = document.createElement("input"); + suche.type = "search"; + suche.className = "form-control form-control-sm mb-2"; + suche.placeholder = "Suchen – Name, Raum oder Geräteart"; + const liste = document.createElement("div"); + liste.className = "list-group list-group-flush"; + liste.style.maxHeight = "15rem"; + liste.style.overflowY = "auto"; + panel.appendChild(suche); + panel.appendChild(liste); + // Hinter die ganze Zeile haengen, nicht in sie hinein: in einer + // input-group stuende die Liste sonst als weitere Spalte daneben. + knopf.closest(".input-group").after(panel); + + const eintrag = (d, mitEtage) => { + const zeile = document.createElement("button"); + zeile.type = "button"; + // Der Zusatz steht rechts am Rand statt gleich hinter dem Namen: so + // stehen die Raeume untereinander in einer Spalte und lassen sich + // ueberfliegen, statt bei jeder Zeile woanders anzufangen. + zeile.className = "list-group-item list-group-item-action py-1" + + " d-flex justify-content-between align-items-center gap-2" + + (d.id === aktuelleId ? " active" : ""); + zeile.innerHTML = "" + textSicher(d.label) + "" + + "" + + textSicher(zeilenZusatz(d, mitEtage)) + ""; + zeile.onclick = () => { schliesseWaehler(); beiAuswahl(d); }; + return zeile; + }; + + const zeichne = () => { + const suchtext = suche.value.trim().toLowerCase(); + liste.innerHTML = ""; + const passend = geraete.filter(d => !suchtext || suchfeld(d).includes(suchtext)); + if (!passend.length) { + liste.innerHTML = "
Nichts gefunden.
"; + return; + } + // Beim Suchen die Zwischenueberschriften weglassen: die Treffer stehen + // dann quer ueber alle Etagen. Dafuer traegt jede Zeile die Etage mit - + // "Bad" allein sagt sonst nicht, welches der drei Baeder gemeint ist. + if (suchtext) { + passend.forEach(d => liste.appendChild(eintrag(d, true))); + return; + } + geraeteGruppen(passend).forEach(g => { + const kopf = document.createElement("div"); + kopf.className = "px-2 pt-2 pb-1 small fw-bold text-body-secondary"; + kopf.textContent = g.titel; + liste.appendChild(kopf); + g.geraete.forEach(d => liste.appendChild(eintrag(d, false))); + }); + }; + + suche.oninput = zeichne; + suche.onkeydown = e => { + if (e.key === "Escape") { schliesseWaehler(); knopf.focus(); } + if (e.key === "Enter") { + e.preventDefault(); + const erster = liste.querySelector("button.list-group-item"); + if (erster) erster.click(); + } + }; + zeichne(); + suche.focus(); +} + +// Klick daneben schliesst die Liste - sonst bliebe sie stehen, wenn man es +// sich anders ueberlegt. +document.addEventListener("click", e => { + if (e.target.closest(".geraete-waehler") || e.target.closest(".form-select.text-start")) return; + schliesseWaehler(); +}); + +function textSicher(text) { + return String(text).replace(/&/g, "&").replace(//g, ">"); +} + +/** + * Wertfeld passend zum Datentyp des Messwerts oder Parameters. + * Liefert das fertige Element; der Aufrufer haengt seinen Listener an. + */ +function wertFeld(spec, wert) { + let feld; + if (spec.input === "bool") { + feld = document.createElement("select"); + feld.className = "form-select"; + optionListe(feld, [{ value: "true", text: "JA" }, { value: "false", text: "NEIN" }], + wert === "" ? "true" : wert); + } else if (spec.input === "select") { + // Die Auswahl kommt fertig aus dem Katalog: bei Messwerten steht der + // Klartext auch als Wert, bei Geraeteparametern die Zahl dahinter. + feld = document.createElement("select"); + feld.className = "form-select"; + optionListe(feld, spec.options.map(o => ({ value: o.value, text: o.label })), wert); + } else { + feld = document.createElement("input"); + feld.className = "form-control"; + feld.type = spec.input; + if (spec.min !== undefined && spec.min !== null) feld.min = spec.min; + if (spec.max !== undefined && spec.max !== null) feld.max = spec.max; + feld.value = wert !== "" ? wert + : spec.input === "time" ? "00:00" + : spec.input === "date" ? new Date().toISOString().split("T")[0] + : spec.input === "number" ? "0" : ""; + } + return feld; +} + +/** Standardwert, wenn eine Zeile auf einen anderen Messwert umgestellt wird. */ +function standardWert(spec) { + if (spec.input === "bool") return "true"; + if (spec.input === "select") return spec.options.length ? spec.options[0].value : ""; + if (spec.input === "time") return "00:00"; + if (spec.input === "date") return new Date().toISOString().split("T")[0]; + if (spec.input === "number") return "0"; + return ""; +} + +// --- Ausloeser ----------------------------------------------------------- + +/** + * Zeichnet alle UND-Bloecke. Jeder Block hat sein eigenes "+ Bedingung", denn + * mit einem einzigen Knopf ganz unten kaeme man immer nur an den letzten + * Block heran und koennte eine bestehende Alternative nie mehr ergaenzen. + */ +function renderConditions() { + const ziel = document.getElementById("condBlocks"); + ziel.innerHTML = ""; + + autoModel.groups.forEach((gruppe, gi) => { + if (gi > 0) { + const trenner = document.createElement("div"); + trenner.className = "d-flex align-items-center my-2 text-body-secondary"; + trenner.innerHTML = "
" + + "ODER" + + "
"; + ziel.appendChild(trenner); + } + + const block = document.createElement("div"); + block.className = "border rounded p-2"; + + gruppe.forEach((bed, bi) => { + if (bi > 0) { + const und = document.createElement("div"); + und.className = "small fw-bold text-body-secondary ms-1 my-1"; + und.textContent = "und"; + block.appendChild(und); + } + block.appendChild(conditionRow(gi, bi, bed)); + }); + + const add = document.createElement("button"); + add.type = "button"; + add.className = "btn btn-outline-success btn-sm mt-2"; + add.innerHTML = " Bedingung"; + add.onclick = () => { gruppe.push(leereBedingung()); renderConditions(); }; + block.appendChild(add); + + ziel.appendChild(block); + }); + + renderSummary(); +} + +function conditionRow(gi, bi, bed) { + const zeile = document.createElement("div"); + zeile.className = "input-group auto-zeile"; + + const treffer = stateById(bed.state_id); + const dev = treffer ? treffer.device : null; + const state = treffer ? treffer.state : null; + + // Geraet + zeile.appendChild(geraeteWaehler( + autoDevices.filter(d => d.states.length), dev ? dev.id : 0, + neu => { + bed.state_id = neu.states[0].id; + bed.operator = neu.states[0].operators[0].value; + bed.value = standardWert(neu.states[0]); + renderConditions(); + })); + + // Messwert - nur zeigen, wenn das Geraet mehr als einen hat + if (dev && dev.states.length > 1) { + const messwert = document.createElement("select"); + messwert.className = "form-select auto-messwert"; + optionListe(messwert, dev.states.map(s => ({ + value: s.id, + text: s.name + (s.value ? " (= " + s.value + ")" : "") + })), bed.state_id); + messwert.onchange = () => { + const neu = dev.states.find(s => s.id === Number(messwert.value)); + bed.state_id = neu.id; + bed.operator = neu.operators[0].value; + bed.value = standardWert(neu); + renderConditions(); + }; + zeile.appendChild(messwert); + } + + if (state) { + // Operator. Eine Auswahlliste statt des frueheren Umschalt-Knopfes: der + // zeigte nur seinen aktuellen Zustand, welche Vergleiche es sonst noch + // gibt, fand man erst durch Klicken heraus. + const op = document.createElement("select"); + // Feste, schmale Breite: der Pfeil der Auswahlliste braucht Platz, sonst + // schiebt er das Zeichen aus dem sichtbaren Bereich. + op.className = "form-select flex-grow-0 text-center"; + op.style.flex = "0 0 5.5rem"; + optionListe(op, state.operators.map(o => ({ value: o.value, text: o.label })), bed.operator); + op.onchange = () => { bed.operator = op.value; renderSummary(); }; + zeile.appendChild(op); + + const wert = wertFeld(state, bed.value); + wert.onchange = () => { bed.value = wert.value; renderSummary(); }; + zeile.appendChild(wert); + + if (state.unit) { + const einheit = document.createElement("span"); + einheit.className = "input-group-text"; + einheit.textContent = state.unit; + zeile.appendChild(einheit); + } + } + + const weg = document.createElement("button"); + weg.type = "button"; + weg.className = "btn btn-outline-danger"; + weg.innerHTML = ""; + weg.onclick = () => { + autoModel.groups[gi].splice(bi, 1); + // Ein leerer Block hat keine Bedeutung mehr und verschwindet mit. + if (!autoModel.groups[gi].length) autoModel.groups.splice(gi, 1); + renderConditions(); + }; + zeile.appendChild(weg); + + return zeile; +} + +/** + * Derselbe Satz, den auch die Uebersicht und der Runner benutzen. Er steht + * unter den Bloecken, damit man die Klammerung nicht aus der Optik erschliessen + * muss, sondern schwarz auf weiss liest. + */ +function conditionText() { + const teile = autoModel.groups + .map(g => g.filter(bed => stateById(bed.state_id))) + .filter(g => g.length) + .map(gruppe => { + const text = gruppe.map(bed => { + const treffer = stateById(bed.state_id); + let s = treffer.device.label + ": " + treffer.state.name + " " + + operatorLabel(treffer.state, bed.operator) + " " + bed.value; + if (treffer.state.unit) s += " " + treffer.state.unit; + return s; + }).join(" und "); + return (autoModel.groups.length > 1 && gruppe.length > 1) ? "(" + text + ")" : text; + }); + return teile.join(" oder "); +} + +function renderSummary() { + const text = conditionText(); + document.getElementById("condSummary").innerHTML = text + ? "Löst aus, wenn " + text.replace(/&/g, "&").replace(/." + : "Noch kein Auslöser – die Automatik würde nie starten."; +} + +// --- Aktionen ------------------------------------------------------------ + +function renderActions() { + const ziel = document.getElementById("actionList"); + ziel.innerHTML = ""; + autoModel.actions.forEach((aktion, ai) => ziel.appendChild(actionRow(ai, aktion))); +} + +function actionRow(ai, aktion) { + const zeile = document.createElement("div"); + zeile.className = "input-group mb-2 auto-zeile"; + + const treffer = commandById(aktion.command_id); + const dev = treffer ? treffer.device : null; + const cmd = treffer ? treffer.command : null; + + zeile.appendChild(geraeteWaehler( + autoDevices.filter(d => d.commands.length), dev ? dev.id : 0, + neu => { + aktion.command_id = neu.commands[0].id; + aktion.params = standardParams(neu.commands[0]); + renderActions(); + })); + + if (dev && dev.commands.length > 1) { + const befehl = document.createElement("select"); + befehl.className = "form-select auto-messwert"; + optionListe(befehl, dev.commands.map(c => ({ value: c.id, text: c.name })), aktion.command_id); + befehl.onchange = () => { + const neu = dev.commands.find(c => c.id === Number(befehl.value)); + aktion.command_id = neu.id; + aktion.params = standardParams(neu); + renderActions(); + }; + zeile.appendChild(befehl); + } + + // Je Parameter ein Feld - wie viele es sind, sagt das Geraet. Frueher gab es + // vier feste Felder "Wert 1" bis "Wert 4", die je nach Kommando versteckt + // wurden, ihren Inhalt aber trotzdem mitschickten. + if (cmd) { + cmd.params.forEach(p => { + const beschriftung = document.createElement("span"); + beschriftung.className = "input-group-text"; + beschriftung.textContent = p.name; + zeile.appendChild(beschriftung); + + const wert = wertFeld(p, aktion.params[p.id] !== undefined ? aktion.params[p.id] : standardWert(p)); + wert.onchange = () => { aktion.params[p.id] = wert.value; }; + zeile.appendChild(wert); + }); + } + + const weg = document.createElement("button"); + weg.type = "button"; + weg.className = "btn btn-outline-danger"; + weg.innerHTML = ""; + weg.onclick = () => { autoModel.actions.splice(ai, 1); renderActions(); }; + zeile.appendChild(weg); + + return zeile; +} + +function standardParams(cmd) { + const params = {}; + cmd.params.forEach(p => { params[p.id] = standardWert(p); }); + return params; +} + +// --- Rahmenbedingungen --------------------------------------------------- + +const WOCHENTAGE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; + +function renderWeekdays() { + const ziel = document.getElementById("weekdayGroup"); + ziel.innerHTML = ""; + WOCHENTAGE.forEach((tag, i) => { + const feld = document.createElement("div"); + feld.className = "input-group-text"; + feld.innerHTML = "" + + ""; + feld.querySelector("input").onchange = e => { + if (e.target.checked) autoModel.weekdays |= (1 << i); + else autoModel.weekdays &= ~(1 << i); + }; + ziel.appendChild(feld); + }); +} + +function fuelleFormular() { + const sperre = document.getElementById("lockoutSecs"); + optionListe(sperre, autoLockouts.map(o => ({ value: o.value, text: o.label })), + autoModel.lockout_secs); + document.getElementById("autoName").value = autoModel.name; + // Kennt die Liste die Etage nicht, bleibt eine Auswahlliste sonst leer + // stehen und schickt beim Speichern "" mit. + const etage = document.getElementById("autoFloor"); + etage.value = autoModel.floor; + if (etage.selectedIndex < 0) { + etage.selectedIndex = 0; + autoModel.floor = etage.value; + } + document.getElementById("tSpanFrom").value = autoModel.window_from; + document.getElementById("tSpanTo").value = autoModel.window_to; + document.getElementById("actFerien").checked = autoModel.on_vacation; + document.getElementById("actFeier").checked = autoModel.on_holiday; + document.getElementById("runOnce").checked = autoModel.force_once; + renderWeekdays(); + renderConditions(); + renderActions(); +} + +/** Die Felder, die direkt am Formular haengen, ins Modell zurueckholen. */ +function leseFormular() { + autoModel.name = document.getElementById("autoName").value.trim(); + autoModel.floor = document.getElementById("autoFloor").value; + autoModel.window_from = document.getElementById("tSpanFrom").value; + autoModel.window_to = document.getElementById("tSpanTo").value; + autoModel.on_vacation = document.getElementById("actFerien").checked; + autoModel.on_holiday = document.getElementById("actFeier").checked; + autoModel.force_once = document.getElementById("runOnce").checked; + autoModel.lockout_secs = Number(document.getElementById("lockoutSecs").value); +} + +// --- Modal --------------------------------------------------------------- + +function loadAutomatic() { + const daten = JSON.parse(document.getElementById("autoActionData").textContent); + autoDevices = daten.devices; + autoLockouts = daten.lockouts; + autoRooms = daten.rooms || []; + + const a = daten.automation; + autoModel = { + id: a.id, + name: a.name, + floor: a.floor, + enabled: a.enabled, + window_from: a.window_from, + window_to: a.window_to, + weekdays: a.weekdays, + on_vacation: a.on_vacation, + on_holiday: a.on_holiday, + force_once: a.force_once, + lockout_secs: a.lockout_secs, + groups: [], + actions: a.actions.map(x => ({ command_id: x.command_id, params: Object.assign({}, x.params) })) + }; + + // Die flache Liste aus der Datenbank wieder in Bloecke fassen. Die + // Gruppennummern kommen aufsteigend und dicht, siehe saveAutomation(). + const nachGruppe = new Map(); + a.conditions.forEach(c => { + if (!nachGruppe.has(c.group)) nachGruppe.set(c.group, []); + nachGruppe.get(c.group).push({ state_id: c.state_id, operator: c.operator, value: c.value }); + }); + autoModel.groups = Array.from(nachGruppe.values()); + + // Eine neue Automatik startet mit je einer leeren Zeile, sonst steht man + // vor einem leeren Formular ohne erkennbaren Anfang. + if (!autoModel.groups.length) autoModel.groups = [[leereBedingung()]]; + if (!autoModel.actions.length) autoModel.actions = [leereAktion()]; + + fuelleFormular(); +} + +function openAutoActionModal(params) { + const contentURL = "./ajax/AutoAction.php" + params; + document.getElementById("modal-title").innerHTML = + params.search("id=") > 0 ? "Automatik bearbeiten" : "Neue Automatik anlegen"; + + // Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne + // Zuhoerer stellt sicher, dass nicht auch noch der Handler eines vorher + // geoeffneten Modals mitfeuert. + const alt = document.getElementById("modalSaveBtn"); + const btn = alt.cloneNode(true); + alt.replaceWith(btn); + // Das Raum-Modal versteckt ihn; hier wird er gebraucht. + btn.hidden = false; + btn.addEventListener("click", submitAutoAction); + + // Der Editor braucht mehr Breite als die Ladedialoge, gibt sie aber wieder + // her, damit die anderen Modals unveraendert aussehen. + const dialog = document.querySelector("#modalEV .modal-dialog"); + dialog.classList.add("modal-xl"); + document.getElementById("modalEV").addEventListener("hidden.bs.modal", + () => dialog.classList.remove("modal-xl"), { once: true }); + + const modalBodyElement = document.getElementById("modal-body"); + modalBodyElement.innerHTML = loadingHTML("Wird geladen..."); + modalEV.show(); + + fetch(contentURL, { + method: "GET", + headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" } + }) + .then(response => response.text()) + .then(html => { + modalBodyElement.innerHTML = html; + loadAutomatic(); + document.getElementById("btnAddGroup").addEventListener("click", () => { + autoModel.groups.push([leereBedingung()]); + renderConditions(); + }); + document.getElementById("btnAddActor").addEventListener("click", () => { + autoModel.actions.push(leereAktion()); + renderActions(); + }); + }) + .catch(error => { + modalBodyElement.innerHTML = "Konnte den Editor nicht laden: " + error.message; + }); +} + +function submitAutoAction() { + leseFormular(); + const nutzlast = { + id: autoModel.id, + name: autoModel.name, + floor: autoModel.floor, + enabled: autoModel.enabled, + window_from: autoModel.window_from, + window_to: autoModel.window_to, + weekdays: autoModel.weekdays, + on_vacation: autoModel.on_vacation, + on_holiday: autoModel.on_holiday, + force_once: autoModel.force_once, + lockout_secs: autoModel.lockout_secs, + conditions: [], + actions: autoModel.actions + }; + autoModel.groups.forEach((gruppe, gi) => { + gruppe.forEach(bed => { + nutzlast.conditions.push({ group: gi, state_id: bed.state_id, operator: bed.operator, value: bed.value }); + }); + }); + + automationRequest("save", nutzlast).then(antwort => { + if (antwort.error) { + alert(antwort.error); + return; + } + modalEV.hide(); + refreshAutomations(nutzlast.floor); + }); +} + +// --- Uebersicht ---------------------------------------------------------- + +function automationRequest(action, body) { + return fetch("./ajax/AutoAction.php?action=" + action, { + method: "POST", + headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" }, + body: JSON.stringify(body) + }) + .then(response => response.json()) + .catch(error => ({ error: error.message })); +} + +/** + * Die Tabelle einer Etage neu holen. Ohne Etage alle, die es gibt. + * + * Welche das sind, sagen die Reiter - home.php baut sie aus der Raumtabelle. + * Eine feste Liste hier haette beim Aussengelaende still eine Etage + * ausgelassen: die Tabelle waere leer geblieben, ohne Fehlermeldung. + */ +function refreshAutomations(floor) { + const etagen = floor ? [floor] + : Array.from(document.querySelectorAll("#actions-tabContent .tab-pane")) + .map(pane => pane.id.replace(/^actions-/, "")); + etagen.forEach(etage => { + const ziel = document.getElementById("actions-" + etage + "-list"); + if (!ziel) return; + fetch("./ajax/AutoAction.php?action=list&floor=" + etage, { + method: "GET", + headers: { "Requested-With-Ajax": "ajax" } + }) + .then(response => response.text()) + .then(html => { ziel.innerHTML = html; }) + .catch(() => { ziel.innerHTML = "

Liste konnte nicht geladen werden.

"; }); + }); +} + +/** + * Zuordnung Geraet -> Raum. Eigenes Modal, weil es kein Teil einer einzelnen + * Automatik ist, sondern einmal fuer alle gilt. + */ +function openRoomModal() { + document.getElementById("modal-title").innerHTML = "Geräte den Räumen zuordnen"; + const alt = document.getElementById("modalSaveBtn"); + const btn = alt.cloneNode(true); + alt.replaceWith(btn); + // Das Raum-Modal versteckt ihn; hier wird er gebraucht. + btn.hidden = false; + btn.addEventListener("click", speichereRaeume); + + const dialog = document.querySelector("#modalEV .modal-dialog"); + dialog.classList.add("modal-lg"); + document.getElementById("modalEV").addEventListener("hidden.bs.modal", + () => dialog.classList.remove("modal-lg"), { once: true }); + + const koerper = document.getElementById("modal-body"); + koerper.innerHTML = loadingHTML("Wird geladen..."); + modalEV.show(); + + fetch("./ajax/AutoAction.php?action=rooms", { + method: "GET", headers: { "Requested-With-Ajax": "ajax" } + }) + .then(a => a.text()) + .then(html => { + koerper.innerHTML = html; + const tipps = document.getElementById("btnRoomTipps"); + if (tipps) { + tipps.addEventListener("click", () => { + koerper.querySelectorAll("select[data-tipp]").forEach(s => { + if (!s.value) s.value = s.dataset.tipp; + }); + tipps.disabled = true; + }); + } + }) + .catch(fehler => { koerper.innerHTML = "Konnte die Zuordnung nicht laden: " + fehler.message; }); +} + +function speichereRaeume() { + const rooms = {}; + document.querySelectorAll("#modal-body select[data-actor]").forEach(s => { + rooms[s.dataset.actor] = s.value; + }); + automationRequest("saveRooms", { rooms: rooms }).then(antwort => { + if (antwort.error) alert(antwort.error); + else modalEV.hide(); + }); +} + +function toggleAutomation(id) { + automationRequest("toggle", { id: id }).then(antwort => { + if (antwort.error) alert(antwort.error); + else refreshAutomations(null); + }); +} + +function deleteAutomation(id, name) { + if (!confirm("Automatik \"" + name + "\" wirklich löschen?")) return; + automationRequest("delete", { id: id }).then(antwort => { + if (antwort.error) alert(antwort.error); + else refreshAutomations(null); + }); +} + +document.addEventListener("DOMContentLoaded", () => refreshAutomations(null)); diff --git a/js/solar/heatMQTT.js b/js/solar/heatMQTT.js index 5929059..38f086b 100644 --- a/js/solar/heatMQTT.js +++ b/js/solar/heatMQTT.js @@ -1,193 +1,193 @@ -var mqttData = {}; - -const solarMQTT = { - getMQTT: function () { - const id = Math.random().toString(36).substring(7); - const topic = "#"; - const connection = "wss://mqtt.nas.el-wa.org:443" - mqttsolarTreeDone = false; - // const connection = "ws://username:password@37.97.203.138:8083" // Works - // const connection = "wss://public:public@public.cloud.shiftr.io" // Works - const client = mqtt.connect(connection, { - rejectUnauthorized: false, - }); - - client.on("message", messageReceived); - client.on("connect", function () { - client.subscribe("solarManager/#"); - client.subscribe("wattpilot/properties/lmo/state"); - client.subscribe("wattpilot/properties/ftt/state"); - client.subscribe("wattpilot/properties/fte/state"); - client.subscribe("wattpilot/properties/amp/state"); - client.subscribe("wattpilot/properties/car/state"); - client.subscribe("go-eCharger/270003/amp"); - client.subscribe("go-eCharger/270003/ate"); - client.subscribe("go-eCharger/270003/lmo"); - client.subscribe("go-eCharger/270003/att"); - client.subscribe("go-eCharger/270003/car"); - client.subscribe("weatherStation/#"); - }); - client.on("error", function (error) { - //alert("MQTT Error: " + error); - }); - client.on('end', function () { - setTimeout(getMQTT, 5000); - alert("MQTT Disconnected, try to reconnect in 5 secs."); - }) - - function getNestedProp(obj, path) { - return path.split('/').reduce((acc, key) => acc && acc[key], obj); - } - function setNestedProp(obj, path, value) { - var schema = obj; // a moving reference to internal objects within obj - var pList = path.split('/'); - var len = pList.length; - for (var i = 0; i < len - 1; i++) { - var elem = pList[i]; - if (!schema[elem]) schema[elem] = {} - schema = schema[elem]; - } - - schema[pList[len - 1]] = value; - } - - function messageReceived(topic, message) { - setNestedProp(mqttData, topic, message); - if (topic == "solarManager/P_Load") { - setTimeout(function () { solarSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values - } else if (topic == "weatherStation/windDeg") { - setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation"]?.[name]) }, 200); - } - } - } -} - -const solarSVG = { - updateCnt: 99, - updateValuesMQTT: function (mqttData) { - if (this.updateCnt > 10) { - this.updateCnt = 0; - var obj = document.querySelector("object"); - var htmlNode = obj.contentDocument; - htmlNode.getElementById("PufferOtxt").innerHTML = mqttData["solarManager"]["t_buffT"] + " °C"; - htmlNode.getElementById("PufferMtxt").innerHTML = mqttData["solarManager"]["t_buffM"] + " °C"; - htmlNode.getElementById("PufferUtxt").innerHTML = mqttData["solarManager"]["t_buffB"] + " °C"; - htmlNode.getElementById("heaterVL").innerHTML = mqttData["solarManager"]["t_heatVL"] + " °C"; - htmlNode.getElementById("heaterRL").innerHTML = mqttData["solarManager"]["t_heatRL"] + " °C"; - htmlNode.getElementById("thermeVLfb").innerHTML = mqttData["solarManager"]["t_gasVLu"] + " °C"; - htmlNode.getElementById("thermeVLww").innerHTML = mqttData["solarManager"]["t_gasVLo"] + " °C"; - htmlNode.getElementById("thermeRL").innerHTML = mqttData["solarManager"]["t_gasRL"] + " °C"; - htmlNode.getElementById("fbVL").innerHTML = mqttData["solarManager"]["t_fbVL"] + " °C"; - htmlNode.getElementById("fbRL").innerHTML = mqttData["solarManager"]["t_fbRL"] + " °C"; - htmlNode.getElementById("triac").innerHTML = mqttData["solarManager"]["t_triac"] + " °C"; - } - this.updateCnt++; - } - -} - -var chartSettings = { - type: 'line', - options: { - animation: true, - plugins: { - annotation: { - common: { type: 'box', drawTime: 'beforeDatasetsDraw', yScaleID: 'y-axis-0', backgroundColor: 'rgba(255, 255, 255, 0.05)', init: true }, - annotations: [] - }, - tooltip: { - position: 'nearest', - pointStyle: "circle", - boxWidth: 4, - usePointStyle: true, - callbacks: { - label: function (context) { - let label = context.dataset.label || ''; - if (label) { - label += ': '; - } - if (context.dataset.yAxisID == "y1") { - label += Math.round(context.parsed.y * 10) / 10 + " " + "L/min"; - } else { - if (context.parsed.y !== null) { - ret = scale(Math.round(context.parsed.y), false); - label += ret[0] + " " + ret[1] + "°C"; - } - } - return label; - }, - }, - }, - legend: { - position: "bottom", - labels: { - pointStyleWidth: 10, - usePointStyle: true, - pointStyle: "line", - } - }, - }, - responsive: true, - maintainAspectRatio: false, - interaction: { - intersect: false, - mode: 'index', - }, - scales: { - x: { - adapters: { - date: { - locale: "DE-de" - } - }, - ticks: { - - }, - type: 'timestack', - }, - y: { - stacked: false, - display: true, - position: 'left', - ticks: { - callback: value => `${value} °C`, - }, - title: { - display: true, - text: "Temperatur" - } - }, - y1: { - stacked: false, - display: true, - position: 'right', - ticks: { - callback: value => `${value} L/min`, - }, - title: { - display: true, - text: "Wasserverbrauch" - }, - data:{} - } - } - } - }; - - var chartData = {}; - const heatChart = new Chart( - document.querySelector('#heat-chart'), - Object.assign({}, chartSettings) - ); - const waterChart = new Chart( - document.querySelector('#water-chart'), - Object.assign({}, chartSettings) - ); - - document.addEventListener('readystatechange', function () { - if (event.target.readyState === "complete") { - solarMQTT.getMQTT(); - getData(heatChart, 'ajax/getHeaterData.php', { sunrise: { FROM: -24, TO: 0 } }); - getData(waterChart, 'ajax/getWaterData.php', { sunrise: { FROM: -24, TO: 0 } }); - } - }); +var mqttData = {}; + +const solarMQTT = { + getMQTT: function () { + const id = Math.random().toString(36).substring(7); + const topic = "#"; + const connection = "wss://mqtt.nas.el-wa.org:443" + mqttsolarTreeDone = false; + // const connection = "ws://username:password@37.97.203.138:8083" // Works + // const connection = "wss://public:public@public.cloud.shiftr.io" // Works + const client = mqtt.connect(connection, { + rejectUnauthorized: false, + }); + + client.on("message", messageReceived); + client.on("connect", function () { + client.subscribe("solarManager/#"); + client.subscribe("wattpilot/properties/lmo/state"); + client.subscribe("wattpilot/properties/ftt/state"); + client.subscribe("wattpilot/properties/fte/state"); + client.subscribe("wattpilot/properties/amp/state"); + client.subscribe("wattpilot/properties/car/state"); + client.subscribe("go-eCharger/270003/amp"); + client.subscribe("go-eCharger/270003/ate"); + client.subscribe("go-eCharger/270003/lmo"); + client.subscribe("go-eCharger/270003/att"); + client.subscribe("go-eCharger/270003/car"); + client.subscribe("weatherStation/#"); + }); + client.on("error", function (error) { + //alert("MQTT Error: " + error); + }); + client.on('end', function () { + setTimeout(getMQTT, 5000); + alert("MQTT Disconnected, try to reconnect in 5 secs."); + }) + + function getNestedProp(obj, path) { + return path.split('/').reduce((acc, key) => acc && acc[key], obj); + } + function setNestedProp(obj, path, value) { + var schema = obj; // a moving reference to internal objects within obj + var pList = path.split('/'); + var len = pList.length; + for (var i = 0; i < len - 1; i++) { + var elem = pList[i]; + if (!schema[elem]) schema[elem] = {} + schema = schema[elem]; + } + + schema[pList[len - 1]] = value; + } + + function messageReceived(topic, message) { + setNestedProp(mqttData, topic, message); + if (topic == "solarManager/P_Load") { + setTimeout(function () { solarSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values + } else if (topic == "weatherStation/windDeg") { + setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation"]?.[name]) }, 200); + } + } + } +} + +const solarSVG = { + updateCnt: 99, + updateValuesMQTT: function (mqttData) { + if (this.updateCnt > 10) { + this.updateCnt = 0; + var obj = document.querySelector("object"); + var htmlNode = obj.contentDocument; + htmlNode.getElementById("PufferOtxt").innerHTML = mqttData["solarManager"]["t_buffT"] + " °C"; + htmlNode.getElementById("PufferMtxt").innerHTML = mqttData["solarManager"]["t_buffM"] + " °C"; + htmlNode.getElementById("PufferUtxt").innerHTML = mqttData["solarManager"]["t_buffB"] + " °C"; + htmlNode.getElementById("heaterVL").innerHTML = mqttData["solarManager"]["t_heatVL"] + " °C"; + htmlNode.getElementById("heaterRL").innerHTML = mqttData["solarManager"]["t_heatRL"] + " °C"; + htmlNode.getElementById("thermeVLfb").innerHTML = mqttData["solarManager"]["t_gasVLu"] + " °C"; + htmlNode.getElementById("thermeVLww").innerHTML = mqttData["solarManager"]["t_gasVLo"] + " °C"; + htmlNode.getElementById("thermeRL").innerHTML = mqttData["solarManager"]["t_gasRL"] + " °C"; + htmlNode.getElementById("fbVL").innerHTML = mqttData["solarManager"]["t_fbVL"] + " °C"; + htmlNode.getElementById("fbRL").innerHTML = mqttData["solarManager"]["t_fbRL"] + " °C"; + htmlNode.getElementById("triac").innerHTML = mqttData["solarManager"]["t_triac"] + " °C"; + } + this.updateCnt++; + } + +} + +var chartSettings = { + type: 'line', + options: { + animation: true, + plugins: { + annotation: { + common: { type: 'box', drawTime: 'beforeDatasetsDraw', yScaleID: 'y-axis-0', backgroundColor: 'rgba(255, 255, 255, 0.05)', init: true }, + annotations: [] + }, + tooltip: { + position: 'nearest', + pointStyle: "circle", + boxWidth: 4, + usePointStyle: true, + callbacks: { + label: function (context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.dataset.yAxisID == "y1") { + label += Math.round(context.parsed.y * 10) / 10 + " " + "L/min"; + } else { + if (context.parsed.y !== null) { + ret = scale(Math.round(context.parsed.y), false); + label += ret[0] + " " + ret[1] + "°C"; + } + } + return label; + }, + }, + }, + legend: { + position: "bottom", + labels: { + pointStyleWidth: 10, + usePointStyle: true, + pointStyle: "line", + } + }, + }, + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + mode: 'index', + }, + scales: { + x: { + adapters: { + date: { + locale: "DE-de" + } + }, + ticks: { + + }, + type: 'timestack', + }, + y: { + stacked: false, + display: true, + position: 'left', + ticks: { + callback: value => `${value} °C`, + }, + title: { + display: true, + text: "Temperatur" + } + }, + y1: { + stacked: false, + display: true, + position: 'right', + ticks: { + callback: value => `${value} L/min`, + }, + title: { + display: true, + text: "Wasserverbrauch" + }, + data:{} + } + } + } + }; + + var chartData = {}; + const heatChart = new Chart( + document.querySelector('#heat-chart'), + Object.assign({}, chartSettings) + ); + const waterChart = new Chart( + document.querySelector('#water-chart'), + Object.assign({}, chartSettings) + ); + + document.addEventListener('readystatechange', function () { + if (event.target.readyState === "complete") { + solarMQTT.getMQTT(); + getData(heatChart, 'ajax/getHeaterData.php', { sunrise: { FROM: -24, TO: 0 } }); + getData(waterChart, 'ajax/getWaterData.php', { sunrise: { FROM: -24, TO: 0 } }); + } + }); diff --git a/js/solar/historyMQTT.js b/js/solar/historyMQTT.js index dc51248..7c32ec8 100644 --- a/js/solar/historyMQTT.js +++ b/js/solar/historyMQTT.js @@ -1,152 +1,152 @@ -tooltipLabel = function (context) { - let label = context.dataset.label || ''; - if (label) { - label += ': '; - } - if (context.dataset.yAxisID == "y1") { - label += Math.round(context.parsed.y * 10) / 10 + " " + "%"; - } else { - if (context.parsed.y !== null) { - ret = scale(Math.round(context.parsed.y), false); - label += ret[0] + " " + ret[1] + "Wh"; - } - } - return label; -}; - -tooltipFooter = function (tooltipItems){ - let sum = 0; - - tooltipItems.forEach(function(tooltipItem) { - if (tooltipItem.dataset.yAxisID != "y1") { - sum += tooltipItem.parsed.y; - } - }); - ret = scale(Math.round(sum), false); - sum = ret[0] + " " + ret[1] + "Wh"; - return 'Summe: ' + sum; -} - -legendLabels = function(chart){ - const datasets = chart.data.datasets; - const { - labels: { - usePointStyle, - pointStyle, - textAlign, - color - } - } = chart.legend.options; - return chart._getSortedDatasetMetas().map((meta) => { - const style = meta.controller.getStyle(usePointStyle ? 0 : undefined); - const borderWidth = Chart.helpers.toPadding(style.borderWidth); - ret = scale(Math.round(arraySum(datasets[meta.index].data)), false); - ret2 = scale(Math.round(datasets[meta.index].data[datasets[meta.index].data.length-1]), false); - return { - text: datasets[meta.index].label + " Σ " + ret[0]+" "+ret[1]+"Wh",//+ " Last: " + ret2[0]+" "+ret2[1]+"W", - fillStyle: style.backgroundColor, - fontColor: color, - hidden: !meta.visible, - lineCap: style.borderCapStyle, - lineDash: style.borderDash, - lineDashOffset: style.borderDashOffset, - lineJoin: style.borderJoinStyle, - lineWidth: (borderWidth.width + borderWidth.height) / 4, - strokeStyle: style.borderColor, - pointStyle: pointStyle || style.pointStyle, - rotation: style.rotation, - textAlign: textAlign || style.textAlign, - borderRadius: 0, // TODO: v4, default to style.borderRadius - datasetIndex: meta.index - }; - }, this); -} -Chart.defaults.plugins.tooltip.callbacks.footer = tooltipFooter; -Chart.defaults.plugins.tooltip.callbacks.label = tooltipLabel; -Chart.defaults.plugins.legend.labels.generateLabels = legendLabels; - - var forecastChartSettings = { - type: 'bar', - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - intersect: false, - mode: 'index', - }, - scales: { - y: { - stacked: true, - display: true, - min: 0, - suggestedMax: 1000, - ticks: { - callback: value => `${value / 1000} kWh`, - } - }, - } - } - }; - - var decadeChartSettings = { - type: 'bar', - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - intersect: false, - mode: 'index', - }, - scales: { - y: { - stacked: true, - display: true, - min: 0, - suggestedMax: 1000000, - ticks: { - callback: value => `${value / 1000000} MWh`, - } - }, - } - } - }; - - var chartData = {}; - - const consChart = new Chart( - document.querySelector('#consumption-chart'), - Object.assign({}, forecastChartSettings) - ); - const prodChart = new Chart( - document.querySelector('#production-chart'), - Object.assign({}, forecastChartSettings) - ); - const consChartYear = new Chart( - document.querySelector('#consumption-chart-year'), - Object.assign({}, decadeChartSettings) - ); - const prodChartYear = new Chart( - document.querySelector('#production-chart-year'), - Object.assign({}, decadeChartSettings) - ); - const consChartDecade = new Chart( - document.querySelector('#consumption-chart-decade'), - Object.assign({}, decadeChartSettings) - ); - const prodChartDecade = new Chart( - document.querySelector('#production-chart-decade'), - Object.assign({}, decadeChartSettings) - ); - document.addEventListener('readystatechange', function () { - if (event.target.readyState === "complete") { - getData(prodChart, 'ajax/energyHistory.php?series=prod&range=month'); - getData(consChart, 'ajax/energyHistory.php?series=cons&range=month'); - getData(prodChartYear, 'ajax/energyHistory.php?series=prod&range=year'); - getData(consChartYear, 'ajax/energyHistory.php?series=cons&range=year'); - getData(prodChartDecade, 'ajax/energyHistory.php?series=prod&range=decade'); - getData(consChartDecade, 'ajax/energyHistory.php?series=cons&range=decade'); - getStats("Stats-Year","ajax/getStats.php?type=ThisYear"); - getStats("Stats-Lastyear","ajax/getStats.php?type=LastYear"); - getStats("Stats-Prelastyear","ajax/getStats.php?type=PreLastYear"); - } - }); +tooltipLabel = function (context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.dataset.yAxisID == "y1") { + label += Math.round(context.parsed.y * 10) / 10 + " " + "%"; + } else { + if (context.parsed.y !== null) { + ret = scale(Math.round(context.parsed.y), false); + label += ret[0] + " " + ret[1] + "Wh"; + } + } + return label; +}; + +tooltipFooter = function (tooltipItems){ + let sum = 0; + + tooltipItems.forEach(function(tooltipItem) { + if (tooltipItem.dataset.yAxisID != "y1") { + sum += tooltipItem.parsed.y; + } + }); + ret = scale(Math.round(sum), false); + sum = ret[0] + " " + ret[1] + "Wh"; + return 'Summe: ' + sum; +} + +legendLabels = function(chart){ + const datasets = chart.data.datasets; + const { + labels: { + usePointStyle, + pointStyle, + textAlign, + color + } + } = chart.legend.options; + return chart._getSortedDatasetMetas().map((meta) => { + const style = meta.controller.getStyle(usePointStyle ? 0 : undefined); + const borderWidth = Chart.helpers.toPadding(style.borderWidth); + ret = scale(Math.round(arraySum(datasets[meta.index].data)), false); + ret2 = scale(Math.round(datasets[meta.index].data[datasets[meta.index].data.length-1]), false); + return { + text: datasets[meta.index].label + " Σ " + ret[0]+" "+ret[1]+"Wh",//+ " Last: " + ret2[0]+" "+ret2[1]+"W", + fillStyle: style.backgroundColor, + fontColor: color, + hidden: !meta.visible, + lineCap: style.borderCapStyle, + lineDash: style.borderDash, + lineDashOffset: style.borderDashOffset, + lineJoin: style.borderJoinStyle, + lineWidth: (borderWidth.width + borderWidth.height) / 4, + strokeStyle: style.borderColor, + pointStyle: pointStyle || style.pointStyle, + rotation: style.rotation, + textAlign: textAlign || style.textAlign, + borderRadius: 0, // TODO: v4, default to style.borderRadius + datasetIndex: meta.index + }; + }, this); +} +Chart.defaults.plugins.tooltip.callbacks.footer = tooltipFooter; +Chart.defaults.plugins.tooltip.callbacks.label = tooltipLabel; +Chart.defaults.plugins.legend.labels.generateLabels = legendLabels; + + var forecastChartSettings = { + type: 'bar', + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + mode: 'index', + }, + scales: { + y: { + stacked: true, + display: true, + min: 0, + suggestedMax: 1000, + ticks: { + callback: value => `${value / 1000} kWh`, + } + }, + } + } + }; + + var decadeChartSettings = { + type: 'bar', + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + mode: 'index', + }, + scales: { + y: { + stacked: true, + display: true, + min: 0, + suggestedMax: 1000000, + ticks: { + callback: value => `${value / 1000000} MWh`, + } + }, + } + } + }; + + var chartData = {}; + + const consChart = new Chart( + document.querySelector('#consumption-chart'), + Object.assign({}, forecastChartSettings) + ); + const prodChart = new Chart( + document.querySelector('#production-chart'), + Object.assign({}, forecastChartSettings) + ); + const consChartYear = new Chart( + document.querySelector('#consumption-chart-year'), + Object.assign({}, decadeChartSettings) + ); + const prodChartYear = new Chart( + document.querySelector('#production-chart-year'), + Object.assign({}, decadeChartSettings) + ); + const consChartDecade = new Chart( + document.querySelector('#consumption-chart-decade'), + Object.assign({}, decadeChartSettings) + ); + const prodChartDecade = new Chart( + document.querySelector('#production-chart-decade'), + Object.assign({}, decadeChartSettings) + ); + document.addEventListener('readystatechange', function () { + if (event.target.readyState === "complete") { + getData(prodChart, 'ajax/energyHistory.php?series=prod&range=month'); + getData(consChart, 'ajax/energyHistory.php?series=cons&range=month'); + getData(prodChartYear, 'ajax/energyHistory.php?series=prod&range=year'); + getData(consChartYear, 'ajax/energyHistory.php?series=cons&range=year'); + getData(prodChartDecade, 'ajax/energyHistory.php?series=prod&range=decade'); + getData(consChartDecade, 'ajax/energyHistory.php?series=cons&range=decade'); + getStats("Stats-Year","ajax/getStats.php?type=ThisYear"); + getStats("Stats-Lastyear","ajax/getStats.php?type=LastYear"); + getStats("Stats-Prelastyear","ajax/getStats.php?type=PreLastYear"); + } + }); diff --git a/js/solar/homeMQTT.js b/js/solar/homeMQTT.js index 333eebe..713aee1 100644 --- a/js/solar/homeMQTT.js +++ b/js/solar/homeMQTT.js @@ -1,59 +1,59 @@ - -const homeMQTT = { - getMQTT: function () { - const id = Math.random().toString(36).substring(7); - const topic = "#"; - const connection = "wss://mqtt.nas.el-wa.org:443" - mqttsolarTreeDone = false; - // const connection = "ws://username:password@37.97.203.138:8083" // Works - // const connection = "wss://public:public@public.cloud.shiftr.io" // Works - const client = mqtt.connect(connection, { - rejectUnauthorized: false, - }); - - client.on("message", messageReceived); - client.on("connect", function () { - // Welche Zweige gebraucht werden, sagt die Raumtabelle: seit eine - // Kachel jeden Messwert zeigen kann, reicht Raumtemp allein nicht - // mehr. homeTopics kommt aus restricted/rooms.php. - homeTopics.forEach(function (topic) { client.subscribe(topic); }); - }); - client.on("error", function (error) { - //alert("MQTT Error: " + error); - }); - client.on('end', function () { - setTimeout(getMQTT, 5000); - alert("MQTT Disconnected, try to reconnect in 5 secs."); - }) - - function getNestedProp(obj, path) { - return path.split('/').reduce((acc, key) => acc && acc[key], obj); - } - function setNestedProp(obj, path, value) { - var schema = obj; // a moving reference to internal objects within obj - var pList = path.split('/'); - var len = pList.length; - for (var i = 0; i < len - 1; i++) { - var elem = pList[i]; - if (!schema[elem]) schema[elem] = {} - schema = schema[elem]; - } - - schema[pList[len - 1]] = value; - } - - function messageReceived(topic, message) { - setNestedProp(mqttData, topic, message); - setTimeout(function () { homeSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values - } - } -} - -const homeSVG = { - updateCnt: 99, - fillElementArray: function () { - - }, + +const homeMQTT = { + getMQTT: function () { + const id = Math.random().toString(36).substring(7); + const topic = "#"; + const connection = "wss://mqtt.nas.el-wa.org:443" + mqttsolarTreeDone = false; + // const connection = "ws://username:password@37.97.203.138:8083" // Works + // const connection = "wss://public:public@public.cloud.shiftr.io" // Works + const client = mqtt.connect(connection, { + rejectUnauthorized: false, + }); + + client.on("message", messageReceived); + client.on("connect", function () { + // Welche Zweige gebraucht werden, sagt die Raumtabelle: seit eine + // Kachel jeden Messwert zeigen kann, reicht Raumtemp allein nicht + // mehr. homeTopics kommt aus restricted/rooms.php. + homeTopics.forEach(function (topic) { client.subscribe(topic); }); + }); + client.on("error", function (error) { + //alert("MQTT Error: " + error); + }); + client.on('end', function () { + setTimeout(getMQTT, 5000); + alert("MQTT Disconnected, try to reconnect in 5 secs."); + }) + + function getNestedProp(obj, path) { + return path.split('/').reduce((acc, key) => acc && acc[key], obj); + } + function setNestedProp(obj, path, value) { + var schema = obj; // a moving reference to internal objects within obj + var pList = path.split('/'); + var len = pList.length; + for (var i = 0; i < len - 1; i++) { + var elem = pList[i]; + if (!schema[elem]) schema[elem] = {} + schema = schema[elem]; + } + + schema[pList[len - 1]] = value; + } + + function messageReceived(topic, message) { + setNestedProp(mqttData, topic, message); + setTimeout(function () { homeSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values + } + } +} + +const homeSVG = { + updateCnt: 99, + fillElementArray: function () { + + }, /** * Eine Nachricht aus dem Baum holen. mqttData ist nach den Teilen des * Topics verschachtelt, siehe setNestedProp() weiter oben. @@ -116,369 +116,369 @@ const homeSVG = { if (typeof (values["mode"]) != "undefined") el(room.id + "_buffer").setAttribute("display", values["mode"] == "Overheating" ? "" : "none"); }); - } - -} - -var currentFloor = "OG"; - -function addClass(el, classNameToAdd){ - el.className += ' ' + classNameToAdd; -} - -function removeClass(el, classNameToRemove){ - var elClass = ' ' + el.className + ' '; - while(elClass.indexOf(' ' + classNameToRemove + ' ') !== -1){ - elClass = elClass.replace(' ' + classNameToRemove + ' ', ''); - } - el.className = elClass; -} - -// Die Etagen stehen nicht mehr im Code: was es gibt, sagen die Reiter, die -// home.php aus der Raumtabelle gebaut hat. Vorher standen OG, EG und UG hier -// sechsmal, und eine vierte Etage haette sechs neue Zeilen gebraucht. -function switchTab(newtab){ - const newContent = document.getElementById(newtab); - const newTabBtn = document.getElementById(newtab+"-tab"); - document.querySelectorAll("#actions-tab .nav-link").forEach(function(btn){ - removeClass(btn,"active"); - }); - document.querySelectorAll("#actions-tabContent .tab-pane").forEach(function(pane){ - removeClass(pane,"active show"); - }); - addClass(newTabBtn,"active"); - addClass(newContent,"active show"); -} - - -function switchFloor(floor){ - if(currentFloor == floor) - return; - const targetIn = document.getElementById(floor+'_Info'); - const targetOut = document.getElementById(currentFloor+'_Info'); - var blendIn = new KeyframeEffect( - targetIn, [{opacity: '0'},{opacity: '100'}], - { - duration: 500, - easing: "ease-in-out", - fill: "forwards", - iterations: 1, - } - ); - var blendOut = new KeyframeEffect( - targetOut, [{opacity: '100'},{opacity: '0'}], - { - duration: 500, - easing: "ease-in-out", - fill: "forwards", - iterations: 1, - } - ); - var inAnim = new Animation(blendIn,document.timeline); - var outAnim = new Animation(blendOut,document.timeline); - targetIn.setAttribute("display",""); - outAnim.onfinish= (event) => { - targetOut.setAttribute("display","none"); - }; - inAnim.play(); - outAnim.play(); - currentFloor = floor; -} - - - var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), { - keyboard: false - }); - var offcanvas = new bootstrap.Offcanvas(document.getElementById('offcanvas'), { - keyboard: false - }); - - var mqttData = {}; - - function openHeaterSettings(heater){ - openRoomView(heater); - } - - document.addEventListener('readystatechange', function () { - if (event.target.readyState === "complete") { - homeMQTT.getMQTT(); - mountMeteogram(); - } - }); - - - /** - * Alles, was in einem Raum haengt, auf einer Seite: Heizung, Rollladen, - * Licht. Der Grundriss zeigt Raeume, also sollte ein Klick darauf den Raum - * oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben - * Zimmer waren von hier gar nicht erreichbar. - * - * Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room). - */ - function openRoomView(raum) { - const teile = raum.toString().split("_"); - document.getElementById("modal-title").innerHTML = - teile[0] + " · " + teile.slice(1).join(" ") - .replace("ue", "ü").replace("ae", "ä").replace("oe", "ö"); - - // Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne - // Zuhoerer stellt sicher, dass nicht der Handler eines vorher - // geoeffneten Modals mitfeuert; im Raum-Modal bleibt er weg. Hier wird - // nichts gesammelt und dann abgeschickt - jeder Rollladen, jeder - // Schalter und jede Lampe wirkt sofort, und die Solltemperatur tut es - // jetzt auch. Ein Knopf, der nur fuer einen der Reiter gilt, waere in - // allen anderen eine Falle. - const alt = document.getElementById("modalSaveBtn"); - const btn = alt.cloneNode(true); - alt.replaceWith(btn); - btn.hidden = true; - - const koerper = document.getElementById("modal-body"); - koerper.innerHTML = loadingHTML("Wird geladen..."); - modalEV.show(); - - fetch("./ajax/room.php?room=" + encodeURIComponent(raum), { - method: "GET", - headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" } - }) - .then(a => a.text()) - .then(html => { - koerper.innerHTML = html; - - const slider = document.getElementById("modal-slider"); - if (slider) { - bindModalSlider(slider, document.getElementById("modal-slider-label"), - wert => wert + " °C"); - const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"]; - slider.value = typeof stand !== "undefined" ? stand : 10; - // Nur "input": das zeichnet die Anzeige, schickt aber nichts. - slider.dispatchEvent(new Event("input")); - // "change" kommt beim Loslassen - wie bei den Rollladenreglern. - slider.addEventListener("change", () => sendeSolltemperatur(raum)); - } - - raumBedienungAktivieren(koerper); - raumMesswerteAktualisieren(teile[0], teile[1]); - }) - .catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; }); - } - - /** - * Die Bedienelemente des Raum-Modals mit ihren Kommandos verbinden. - * - * Welches Geraet welches Element bekommt, entscheidet der Server - * (restricted/roomControls.php); hier haengen nur die Zuhoerer an den - * data-Attributen, die er dabei setzt. - */ - function raumBedienungAktivieren(koerper) { - koerper.querySelectorAll("button[data-cmd]").forEach(k => { - k.addEventListener("click", () => schalte(k)); - }); - - // Schieberegler: waehrend des Ziehens nur die Anzeige, geschickt wird - // erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel ein - // neues Ziel und kaeme aus dem Anfahren nicht mehr heraus. - koerper.querySelectorAll(".raum-jalousie").forEach(lamellenZeichnen); - koerper.querySelectorAll(".raum-regler").forEach(regler => { - const schieber = regler.querySelector("input[type=range]"); - reglerAnzeigen(regler); - schieber.addEventListener("input", () => reglerAnzeigen(regler)); - schieber.addEventListener("change", () => { - const params = {}; - params[regler.dataset.param] = schieber.value; - raumKommando(Number(regler.dataset.cmd), params, regler); - }); - }); - - // Kippschalter: An und Aus sind zwei getrennte Kommandos. Kennt das - // Geraet nur "umschalten", tut es auch das - dann stimmt die Stellung - // hinterher, weil wir sie selbst gesetzt haben. - koerper.querySelectorAll(".raum-schalter").forEach(schalter => { - const kasten = schalter.querySelector("input[type=checkbox]"); - const beschriftung = schalter.querySelector("label"); - kasten.addEventListener("change", () => { - const ziel = kasten.checked ? schalter.dataset.ein : schalter.dataset.aus; - const cmd = ziel || schalter.dataset.um; - beschriftung.textContent = kasten.checked ? "An" : "Aus"; - if (!cmd) return; - raumKommando(Number(cmd), {}, schalter); - }); - }); - - // Farbe: der Browser bringt seinen Farbwaehler mit, das Geraet will drei - // Zahlen 0-255. Zerlegt wird deshalb erst hier. - koerper.querySelectorAll(".raum-farbe").forEach(feld => { - const eingabe = feld.querySelector("input[type=color]"); - eingabe.addEventListener("change", () => { - const hex = eingabe.value; - const params = {}; - params[feld.dataset.red] = parseInt(hex.substr(1, 2), 16); - params[feld.dataset.green] = parseInt(hex.substr(3, 2), 16); - params[feld.dataset.blue] = parseInt(hex.substr(5, 2), 16); - raumKommando(Number(feld.dataset.cmd), params, feld); - }); - }); - - // Auswahllisten, die ohne zusaetzlichen Knopf schalten - Effekt, Preset. - koerper.querySelectorAll("select.raum-sofort").forEach(auswahl => { - auswahl.addEventListener("change", () => { - const params = {}; - params[auswahl.dataset.param] = auswahl.value; - raumKommando(Number(auswahl.dataset.cmd), params, auswahl); - }); - }); - } - - /** - * Anzeige, gefuellte Bahn und Lamellenbild eines Reglers nachziehen. - * - * Was 0 % und 100 % bedeuten, steht bei den senkrechten Reglern als Wort - * an ihren Enden ("oben"/"unten", "auf"/"zu"); eine Zahl steht dort nicht - * mehr - das Bild daneben zeigt beim Ziehen mehr als sie. - */ - function reglerAnzeigen(regler) { - const schieber = regler.querySelector("input[type=range]"); - const anzeige = regler.querySelector(".raum-regler-wert"); - const wert = Number(schieber.value); - - if (anzeige) { - anzeige.textContent = wert + (schieber.dataset.einheit || ""); - } - - // Der gefuellte Anteil der Bahn - dieselbe Eigenschaft, aus der auch - // bindModalSlider() in common.js den Temperaturregler faerbt. - const min = Number(schieber.min); - const max = Number(schieber.max); - schieber.style.setProperty("--background-size", - ((wert - min) * 100 / (max - min)) + "%"); - - lamellenZeichnen(regler.closest(".raum-jalousie")); - } - - /** Der Stand eines der beiden Regler einer Jalousie, als Anteil 0..1. */ - function reglerAnteil(jalousie, rolle, ersatz) { - const schieber = jalousie.querySelector("[data-rolle=" + rolle + "] input[type=range]"); - if (!schieber) return ersatz; - const min = Number(schieber.min); - return (Number(schieber.value) - min) / (Number(schieber.max) - min); - } - - /** - * Das Fenster zeichnen, wie die Jalousie davor gerade steht. - * - * Zwei Zahlen in einem Bild. Die Hoehe schneidet ab - gezeichnet wird nur - * der Teil des Fensters, den die Jalousie bedeckt, mit der Endschiene an - * seiner Unterkante. Die Neigung bestimmt, wie dick eine Lamelle von vorn - * erscheint: w * sin(Winkel). Bei "auf" sieht man nur ihre Kante und viel - * Luft dazwischen, bei "zu" deckt sie den Abstand zur naechsten - * vollstaendig ab - genau das, was das Licht abhaelt. Von Mitte zu Mitte - * sind es 10 Einheiten, die Lamelle ist 11 tief; ab etwa 65 % schliesst - * das Bild, so wie es die Jalousie auch tut. - * - * Ein Rollladen ohne Neigung bekommt volle Lamellenhoehe - er ist ja ein - * geschlossener Panzer. - */ - function lamellenZeichnen(jalousie) { - if (!jalousie) return; - const bild = jalousie.querySelector(".raum-lamellen"); - if (!bild) return; - - const hoch = bild.viewBox.baseVal.height; - const abdeckung = hoch * reglerAnteil(jalousie, "position", 1); - const kippung = reglerAnteil(jalousie, "neigung", 1); - const dicke = Math.max(1.6, 11 * Math.sin(kippung * Math.PI / 2)); - - bild.querySelector(".raum-abdeckung").setAttribute("height", abdeckung); - bild.querySelectorAll(".raum-lamelle").forEach(lamelle => { - const mitte = Number(lamelle.dataset.mitte); - lamelle.setAttribute("height", dicke); - lamelle.setAttribute("y", mitte - dicke / 2); - }); - // Die Endschiene sitzt auf der Unterkante und bleibt im Bild, auch wenn - // die Jalousie ganz oben steht. - const schiene = bild.querySelector(".raum-endschiene"); - const dick = schiene.height.baseVal.value; - schiene.setAttribute("y", Math.max(0, Math.min(hoch - dick, abdeckung - dick))); - } - - /** - * Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen. - * - * Der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert aus der - * Datenbank waere neben dem Regler nur ein Stand von vorhin. - */ - function raumMesswerteAktualisieren(etage, raum) { - const werte = mqttData["Raumtemp"]?.[etage]?.[raum]; - if (!werte) return; - document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => { - const wert = werte[feld.dataset.mqtt]; - if (typeof wert !== "undefined") feld.textContent = wert; - }); - const heizt = document.querySelector("#roomView [data-mqtt-heizung]"); - if (heizt) heizt.classList.toggle("d-none", String(werte["Heating"]) !== "true"); - } - - /** Ein Kommando des angeklickten Knopfes ausfuehren. */ - function schalte(knopf) { - const zeile = knopf.closest("[data-cmdrow]"); - const params = {}; - if (zeile) { - zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; }); - } - raumKommando(Number(knopf.dataset.cmd), params, knopf); - } - - /** - * Ein Kommando schicken und die Karte so lange sperren. - * - * Ein "Zu" an einer Jalousie dauert ueber eine Minute (die Mechanik will - * zweimal geschwenkt werden, siehe restricted/commands.php). Ohne sichtbare - * Sperre tippt man in der Zeit dreimal nach. - */ - function raumKommando(cmdId, params, element) { - const karte = element.closest(".raum-geraet") || element; - karte.classList.add("raum-warte"); - raumRequest("command", { command_id: cmdId, params: params }) - .then(antwort => { - karte.classList.remove("raum-warte"); - raumMeldung(antwort); - }); - } - - /** - * Die neue Solltemperatur schicken. - * - * Das Modal bleibt danach offen: frueher schloss es sich, weil der - * Speichern-Knopf das Ende der Bedienung war. Jetzt ist das Verschieben - * des Reglers die Bedienung, und wer dreimal nachjustiert, will nicht - * dreimal neu aufmachen. - */ - function sendeSolltemperatur(raum) { - const slider = document.getElementById("modal-slider"); - if (!slider) return; - const karte = slider.closest(".raum-geraet"); - if (karte) karte.classList.add("raum-warte"); - raumRequest("temp", { room: raum, temp: Number(slider.value) }) - .then(antwort => { - if (karte) karte.classList.remove("raum-warte"); - raumMeldung(antwort); - }); - } - - function raumRequest(action, body) { - return fetch("./ajax/room.php?action=" + action, { - method: "POST", - headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" }, - body: JSON.stringify(body) - }) - .then(a => a.json()) - .catch(fehler => ({ error: fehler.message })); - } - - /** Rueckmeldung im Modal statt als alert - man schaltet oft mehrmals. */ - function raumMeldung(antwort) { - const ziel = document.getElementById("roomFeedback"); - if (!ziel) return; - ziel.className = "small mt-2 " + (antwort.error ? "text-danger" : "text-body-secondary"); - ziel.textContent = antwort.error ? antwort.error : "✓ " + antwort.ok; - } + } + +} + +var currentFloor = "OG"; + +function addClass(el, classNameToAdd){ + el.className += ' ' + classNameToAdd; +} + +function removeClass(el, classNameToRemove){ + var elClass = ' ' + el.className + ' '; + while(elClass.indexOf(' ' + classNameToRemove + ' ') !== -1){ + elClass = elClass.replace(' ' + classNameToRemove + ' ', ''); + } + el.className = elClass; +} + +// Die Etagen stehen nicht mehr im Code: was es gibt, sagen die Reiter, die +// home.php aus der Raumtabelle gebaut hat. Vorher standen OG, EG und UG hier +// sechsmal, und eine vierte Etage haette sechs neue Zeilen gebraucht. +function switchTab(newtab){ + const newContent = document.getElementById(newtab); + const newTabBtn = document.getElementById(newtab+"-tab"); + document.querySelectorAll("#actions-tab .nav-link").forEach(function(btn){ + removeClass(btn,"active"); + }); + document.querySelectorAll("#actions-tabContent .tab-pane").forEach(function(pane){ + removeClass(pane,"active show"); + }); + addClass(newTabBtn,"active"); + addClass(newContent,"active show"); +} + + +function switchFloor(floor){ + if(currentFloor == floor) + return; + const targetIn = document.getElementById(floor+'_Info'); + const targetOut = document.getElementById(currentFloor+'_Info'); + var blendIn = new KeyframeEffect( + targetIn, [{opacity: '0'},{opacity: '100'}], + { + duration: 500, + easing: "ease-in-out", + fill: "forwards", + iterations: 1, + } + ); + var blendOut = new KeyframeEffect( + targetOut, [{opacity: '100'},{opacity: '0'}], + { + duration: 500, + easing: "ease-in-out", + fill: "forwards", + iterations: 1, + } + ); + var inAnim = new Animation(blendIn,document.timeline); + var outAnim = new Animation(blendOut,document.timeline); + targetIn.setAttribute("display",""); + outAnim.onfinish= (event) => { + targetOut.setAttribute("display","none"); + }; + inAnim.play(); + outAnim.play(); + currentFloor = floor; +} + + + var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), { + keyboard: false + }); + var offcanvas = new bootstrap.Offcanvas(document.getElementById('offcanvas'), { + keyboard: false + }); + + var mqttData = {}; + + function openHeaterSettings(heater){ + openRoomView(heater); + } + + document.addEventListener('readystatechange', function () { + if (event.target.readyState === "complete") { + homeMQTT.getMQTT(); + mountMeteogram(); + } + }); + + + /** + * Alles, was in einem Raum haengt, auf einer Seite: Heizung, Rollladen, + * Licht. Der Grundriss zeigt Raeume, also sollte ein Klick darauf den Raum + * oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben + * Zimmer waren von hier gar nicht erreichbar. + * + * Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room). + */ + function openRoomView(raum) { + const teile = raum.toString().split("_"); + document.getElementById("modal-title").innerHTML = + teile[0] + " · " + teile.slice(1).join(" ") + .replace("ue", "ü").replace("ae", "ä").replace("oe", "ö"); + + // Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne + // Zuhoerer stellt sicher, dass nicht der Handler eines vorher + // geoeffneten Modals mitfeuert; im Raum-Modal bleibt er weg. Hier wird + // nichts gesammelt und dann abgeschickt - jeder Rollladen, jeder + // Schalter und jede Lampe wirkt sofort, und die Solltemperatur tut es + // jetzt auch. Ein Knopf, der nur fuer einen der Reiter gilt, waere in + // allen anderen eine Falle. + const alt = document.getElementById("modalSaveBtn"); + const btn = alt.cloneNode(true); + alt.replaceWith(btn); + btn.hidden = true; + + const koerper = document.getElementById("modal-body"); + koerper.innerHTML = loadingHTML("Wird geladen..."); + modalEV.show(); + + fetch("./ajax/room.php?room=" + encodeURIComponent(raum), { + method: "GET", + headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" } + }) + .then(a => a.text()) + .then(html => { + koerper.innerHTML = html; + + const slider = document.getElementById("modal-slider"); + if (slider) { + bindModalSlider(slider, document.getElementById("modal-slider-label"), + wert => wert + " °C"); + const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"]; + slider.value = typeof stand !== "undefined" ? stand : 10; + // Nur "input": das zeichnet die Anzeige, schickt aber nichts. + slider.dispatchEvent(new Event("input")); + // "change" kommt beim Loslassen - wie bei den Rollladenreglern. + slider.addEventListener("change", () => sendeSolltemperatur(raum)); + } + + raumBedienungAktivieren(koerper); + raumMesswerteAktualisieren(teile[0], teile[1]); + }) + .catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; }); + } + + /** + * Die Bedienelemente des Raum-Modals mit ihren Kommandos verbinden. + * + * Welches Geraet welches Element bekommt, entscheidet der Server + * (restricted/roomControls.php); hier haengen nur die Zuhoerer an den + * data-Attributen, die er dabei setzt. + */ + function raumBedienungAktivieren(koerper) { + koerper.querySelectorAll("button[data-cmd]").forEach(k => { + k.addEventListener("click", () => schalte(k)); + }); + + // Schieberegler: waehrend des Ziehens nur die Anzeige, geschickt wird + // erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel ein + // neues Ziel und kaeme aus dem Anfahren nicht mehr heraus. + koerper.querySelectorAll(".raum-jalousie").forEach(lamellenZeichnen); + koerper.querySelectorAll(".raum-regler").forEach(regler => { + const schieber = regler.querySelector("input[type=range]"); + reglerAnzeigen(regler); + schieber.addEventListener("input", () => reglerAnzeigen(regler)); + schieber.addEventListener("change", () => { + const params = {}; + params[regler.dataset.param] = schieber.value; + raumKommando(Number(regler.dataset.cmd), params, regler); + }); + }); + + // Kippschalter: An und Aus sind zwei getrennte Kommandos. Kennt das + // Geraet nur "umschalten", tut es auch das - dann stimmt die Stellung + // hinterher, weil wir sie selbst gesetzt haben. + koerper.querySelectorAll(".raum-schalter").forEach(schalter => { + const kasten = schalter.querySelector("input[type=checkbox]"); + const beschriftung = schalter.querySelector("label"); + kasten.addEventListener("change", () => { + const ziel = kasten.checked ? schalter.dataset.ein : schalter.dataset.aus; + const cmd = ziel || schalter.dataset.um; + beschriftung.textContent = kasten.checked ? "An" : "Aus"; + if (!cmd) return; + raumKommando(Number(cmd), {}, schalter); + }); + }); + + // Farbe: der Browser bringt seinen Farbwaehler mit, das Geraet will drei + // Zahlen 0-255. Zerlegt wird deshalb erst hier. + koerper.querySelectorAll(".raum-farbe").forEach(feld => { + const eingabe = feld.querySelector("input[type=color]"); + eingabe.addEventListener("change", () => { + const hex = eingabe.value; + const params = {}; + params[feld.dataset.red] = parseInt(hex.substr(1, 2), 16); + params[feld.dataset.green] = parseInt(hex.substr(3, 2), 16); + params[feld.dataset.blue] = parseInt(hex.substr(5, 2), 16); + raumKommando(Number(feld.dataset.cmd), params, feld); + }); + }); + + // Auswahllisten, die ohne zusaetzlichen Knopf schalten - Effekt, Preset. + koerper.querySelectorAll("select.raum-sofort").forEach(auswahl => { + auswahl.addEventListener("change", () => { + const params = {}; + params[auswahl.dataset.param] = auswahl.value; + raumKommando(Number(auswahl.dataset.cmd), params, auswahl); + }); + }); + } + + /** + * Anzeige, gefuellte Bahn und Lamellenbild eines Reglers nachziehen. + * + * Was 0 % und 100 % bedeuten, steht bei den senkrechten Reglern als Wort + * an ihren Enden ("oben"/"unten", "auf"/"zu"); eine Zahl steht dort nicht + * mehr - das Bild daneben zeigt beim Ziehen mehr als sie. + */ + function reglerAnzeigen(regler) { + const schieber = regler.querySelector("input[type=range]"); + const anzeige = regler.querySelector(".raum-regler-wert"); + const wert = Number(schieber.value); + + if (anzeige) { + anzeige.textContent = wert + (schieber.dataset.einheit || ""); + } + + // Der gefuellte Anteil der Bahn - dieselbe Eigenschaft, aus der auch + // bindModalSlider() in common.js den Temperaturregler faerbt. + const min = Number(schieber.min); + const max = Number(schieber.max); + schieber.style.setProperty("--background-size", + ((wert - min) * 100 / (max - min)) + "%"); + + lamellenZeichnen(regler.closest(".raum-jalousie")); + } + + /** Der Stand eines der beiden Regler einer Jalousie, als Anteil 0..1. */ + function reglerAnteil(jalousie, rolle, ersatz) { + const schieber = jalousie.querySelector("[data-rolle=" + rolle + "] input[type=range]"); + if (!schieber) return ersatz; + const min = Number(schieber.min); + return (Number(schieber.value) - min) / (Number(schieber.max) - min); + } + + /** + * Das Fenster zeichnen, wie die Jalousie davor gerade steht. + * + * Zwei Zahlen in einem Bild. Die Hoehe schneidet ab - gezeichnet wird nur + * der Teil des Fensters, den die Jalousie bedeckt, mit der Endschiene an + * seiner Unterkante. Die Neigung bestimmt, wie dick eine Lamelle von vorn + * erscheint: w * sin(Winkel). Bei "auf" sieht man nur ihre Kante und viel + * Luft dazwischen, bei "zu" deckt sie den Abstand zur naechsten + * vollstaendig ab - genau das, was das Licht abhaelt. Von Mitte zu Mitte + * sind es 10 Einheiten, die Lamelle ist 11 tief; ab etwa 65 % schliesst + * das Bild, so wie es die Jalousie auch tut. + * + * Ein Rollladen ohne Neigung bekommt volle Lamellenhoehe - er ist ja ein + * geschlossener Panzer. + */ + function lamellenZeichnen(jalousie) { + if (!jalousie) return; + const bild = jalousie.querySelector(".raum-lamellen"); + if (!bild) return; + + const hoch = bild.viewBox.baseVal.height; + const abdeckung = hoch * reglerAnteil(jalousie, "position", 1); + const kippung = reglerAnteil(jalousie, "neigung", 1); + const dicke = Math.max(1.6, 11 * Math.sin(kippung * Math.PI / 2)); + + bild.querySelector(".raum-abdeckung").setAttribute("height", abdeckung); + bild.querySelectorAll(".raum-lamelle").forEach(lamelle => { + const mitte = Number(lamelle.dataset.mitte); + lamelle.setAttribute("height", dicke); + lamelle.setAttribute("y", mitte - dicke / 2); + }); + // Die Endschiene sitzt auf der Unterkante und bleibt im Bild, auch wenn + // die Jalousie ganz oben steht. + const schiene = bild.querySelector(".raum-endschiene"); + const dick = schiene.height.baseVal.value; + schiene.setAttribute("y", Math.max(0, Math.min(hoch - dick, abdeckung - dick))); + } + + /** + * Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen. + * + * Der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert aus der + * Datenbank waere neben dem Regler nur ein Stand von vorhin. + */ + function raumMesswerteAktualisieren(etage, raum) { + const werte = mqttData["Raumtemp"]?.[etage]?.[raum]; + if (!werte) return; + document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => { + const wert = werte[feld.dataset.mqtt]; + if (typeof wert !== "undefined") feld.textContent = wert; + }); + const heizt = document.querySelector("#roomView [data-mqtt-heizung]"); + if (heizt) heizt.classList.toggle("d-none", String(werte["Heating"]) !== "true"); + } + + /** Ein Kommando des angeklickten Knopfes ausfuehren. */ + function schalte(knopf) { + const zeile = knopf.closest("[data-cmdrow]"); + const params = {}; + if (zeile) { + zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; }); + } + raumKommando(Number(knopf.dataset.cmd), params, knopf); + } + + /** + * Ein Kommando schicken und die Karte so lange sperren. + * + * Ein "Zu" an einer Jalousie dauert ueber eine Minute (die Mechanik will + * zweimal geschwenkt werden, siehe restricted/commands.php). Ohne sichtbare + * Sperre tippt man in der Zeit dreimal nach. + */ + function raumKommando(cmdId, params, element) { + const karte = element.closest(".raum-geraet") || element; + karte.classList.add("raum-warte"); + raumRequest("command", { command_id: cmdId, params: params }) + .then(antwort => { + karte.classList.remove("raum-warte"); + raumMeldung(antwort); + }); + } + + /** + * Die neue Solltemperatur schicken. + * + * Das Modal bleibt danach offen: frueher schloss es sich, weil der + * Speichern-Knopf das Ende der Bedienung war. Jetzt ist das Verschieben + * des Reglers die Bedienung, und wer dreimal nachjustiert, will nicht + * dreimal neu aufmachen. + */ + function sendeSolltemperatur(raum) { + const slider = document.getElementById("modal-slider"); + if (!slider) return; + const karte = slider.closest(".raum-geraet"); + if (karte) karte.classList.add("raum-warte"); + raumRequest("temp", { room: raum, temp: Number(slider.value) }) + .then(antwort => { + if (karte) karte.classList.remove("raum-warte"); + raumMeldung(antwort); + }); + } + + function raumRequest(action, body) { + return fetch("./ajax/room.php?action=" + action, { + method: "POST", + headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" }, + body: JSON.stringify(body) + }) + .then(a => a.json()) + .catch(fehler => ({ error: fehler.message })); + } + + /** Rueckmeldung im Modal statt als alert - man schaltet oft mehrmals. */ + function raumMeldung(antwort) { + const ziel = document.getElementById("roomFeedback"); + if (!ziel) return; + ziel.className = "small mt-2 " + (antwort.error ? "text-danger" : "text-body-secondary"); + ziel.textContent = antwort.error ? antwort.error : "✓ " + antwort.ok; + } diff --git a/js/solar/solarMQTT.js b/js/solar/solarMQTT.js index 2f337ef..573e250 100644 --- a/js/solar/solarMQTT.js +++ b/js/solar/solarMQTT.js @@ -1,1172 +1,1172 @@ -let solarMqttClient; -let solarSvgUpdatePending = false; - -const solarMQTT = { - getMQTT: function () { - const id = Math.random().toString(36).substring(7); - const topic = "#"; - const connection = "wss://mqtt.nas.el-wa.org:443" - mqttsolarTreeDone = false; - // const connection = "ws://username:password@37.97.203.138:8083" // Works - // const connection = "wss://public:public@public.cloud.shiftr.io" // Works - const client = mqtt.connect(connection, { - rejectUnauthorized: false, - }); - solarMqttClient = client; - - client.on("message", messageReceived); - client.on("connect", function () { - client.subscribe("solarManager/#"); - client.subscribe("wattpilot/properties/lmo/state"); - client.subscribe("wattpilot/properties/ftt/state"); - client.subscribe("wattpilot/properties/fte/state"); - client.subscribe("wattpilot/properties/amp/state"); - client.subscribe("wattpilot/properties/car/state"); - client.subscribe("go-eCharger/270003/amp"); - client.subscribe("go-eCharger/270003/ate"); - client.subscribe("go-eCharger/270003/lmo"); - client.subscribe("go-eCharger/270003/att"); - client.subscribe("go-eCharger/270003/car"); - client.subscribe("weatherStation/#"); - client.subscribe("Gartenwasser/vorn/status"); - client.subscribe("Gartenwasser/vorn/Timers"); - client.subscribe("Gartenwasser/vorn/LastWatering"); - client.subscribe("Gartenwasser/vorn/WateringToday"); - client.subscribe("Gartenwasser/vorn/LastWateringDay"); - client.subscribe("Gartenwasser/hinten/status"); - client.subscribe("Gartenwasser/hinten/Timers"); - client.subscribe("Gartenwasser/hinten/LastWatering"); - client.subscribe("Gartenwasser/hinten/WateringToday"); - client.subscribe("Gartenwasser/hinten/LastWateringDay"); - client.subscribe("Gartenwasser/AutoWatering/#"); - }); - client.on("error", function (error) { - //alert("MQTT Error: " + error); - }); - client.on('end', function () { - setTimeout(getMQTT, 5000); - alert("MQTT Disconnected, try to reconnect in 5 secs."); - }) - - function getNestedProp(obj, path) { - return path.split('/').reduce((acc, key) => acc && acc[key], obj); - } - function setNestedProp(obj, path, value) { - var schema = obj; // a moving reference to internal objects within obj - var pList = path.split('/'); - var len = pList.length; - for (var i = 0; i < len - 1; i++) { - var elem = pList[i]; - if (!schema[elem]) schema[elem] = {} - schema = schema[elem]; - } - - schema[pList[len - 1]] = value; - } - - function messageReceived(topic, message) { - mqttData[topic] = message; - if (topic === "Gartenwasser/vorn/Timers" || topic === "Gartenwasser/hinten/Timers") { - wateringTimersLastSeen[topic] = Date.now(); - } - if (topic.startsWith("Gartenwasser/") && (topic.endsWith("/status") || topic.endsWith("/Timers") || topic.endsWith("/LastWatering") || topic.endsWith("/RainStatus") - || topic.endsWith("/WateringToday") || topic.endsWith("/LastWateringDay"))) { - updateWateringStatus(); - } - if (topic == "solarManager/P_Load") { - if (!solarSvgUpdatePending) { - solarSvgUpdatePending = true; - setTimeout(function () { - solarSvgUpdatePending = false; - solarSVG.updateValuesMQTT(mqttData); - }, 1000); - } - }else if(topic == "weatherStation/windDeg"){ - setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation/" + name]) }, 200); //give the object tree some time to build up and receive all values - } - } - }, - publish: function (topic, payload, callback) { - if (!solarMqttClient || !solarMqttClient.connected) { - throw new Error("MQTT ist noch nicht verbunden."); - } - solarMqttClient.publish(topic, typeof payload === "string" ? payload : JSON.stringify(payload), callback); - } -} - -// Die Knoten im eingebetteten Realtime-SVG bleiben ueber die Lebensdauer der -// Seite bestehen. updateValuesMQTT fasst 104 davon an - bis zu einmal pro -// Sekunde -, deshalb werden sie einmal nachgeschlagen und dann behalten. -const svgElements = {}; -function svgElement(id) { - let node = svgElements[id]; - if (node === undefined) { - node = svgElements[id] = document.getElementById(id); - } - return node; -} - -// Nur schreiben, wenn sich der Wert wirklich geaendert hat. Jeder Schreibzugriff -// macht Style und Paint des Elements ungueltig - bei einem gefilterten Element -// wird dabei sein Filter neu gerastert. Gemessen waren rund 71 % der Zugriffe -// pro Update reine Wiederholungen desselben Werts; die entfallen damit. -const letzterWert = new Map(); -function schreibe(schluessel, wert, anwenden) { - const s = String(wert); - if (letzterWert.get(schluessel) === s) return; - letzterWert.set(schluessel, s); - anwenden(s); -} -// textContent statt innerHTML: setzt einen Textknoten, ohne den HTML-Parser zu bemuehen. -function setText(id, wert) { schreibe("t:" + id, wert, s => { svgElement(id).textContent = s; }); } -function setHTML(id, markup) { schreibe("h:" + id, markup, s => { svgElement(id).innerHTML = s; }); } -function setAttr(id, name, wert) { schreibe("a:" + id + ":" + name, wert, s => { svgElement(id).setAttribute(name, s); }); } -function setDisplay(id, wert) { schreibe("d:" + id, wert, s => { svgElement(id).style.display = s; }); } - - - -// Punktgroesse einer Flusslinie setzen und die Animation anhalten, wenn kein -// Fluss anliegt. --w ist der Durchmesser, die CSS-Regel macht daraus den Radius -// aller Punkte der Gruppe. will-change nur solange die Gruppe laeuft: es fordert -// eine eigene Compositor-Ebene an, und die kostet Speicher. -function setFlowWidth(id, width) { - const flow = svgElement(id); - // Eine negative Breite heisst "kein Fluss in dieser Richtung" - die - // Aufrufer rechnen 55*(1-exp(x)), was bei umgekehrtem Vorzeichen negativ - // wird. Frueher zeichnete eine negative stroke-width schlicht nichts. - // Math.max haelt das bei; Math.abs wuerde stattdessen einen Fluss in der - // falschen Richtung mit voller Staerke anzeigen. - const breite = Math.max(0, width); - const sichtbar = breite >= 0.5; - flow.style.setProperty("--w", breite + "px"); - flow.style.animationPlayState = sichtbar ? "running" : "paused"; - flow.style.willChange = sichtbar ? "transform" : "auto"; -} - -const solarSVG = { - updateCnt: 99, - /** - * Beide Zwischenspeicher verwerfen - noetig, falls das SVG neu gezeichnet - * wird. Nur die Knoten zu vergessen reicht nicht: letzterWert wuerde dann - * jeden Schreibzugriff als "unveraendert" ueberspringen und das frische - * SVG behielte seine Platzhalter. - */ - fillElementArray: function () { - for (const id in svgElements) delete svgElements[id]; - letzterWert.clear(); - }, - updateValuesMQTT: function (mqttData) { - - const angleFactor = 15000 / 288; - const sixkWangleFactor = 6000 / 288; - var cons = Number(mqttData["solarManager/P_Load"]); - var pvn = JSON.parse(mqttData["solarManager/P_PVn"]); - var pv = Number(mqttData["solarManager/P_PV"]); - var pv2 = Number(pvn[1]); - var pv1 = Number(pvn[0]); - var pv3 = pv - (pv1 + pv2); - var pbatt = Number(mqttData["solarManager/P_Akku"]); - var limbatt = Number(mqttData["solarManager/crgMaxPct"]); - var grid = Number(mqttData["solarManager/P_Grid"]); - var soc = Number(mqttData["solarManager/SOC"]); - var og = Number(mqttData["solarManager/og"]); - var eg = Number(mqttData["solarManager/eg"]); - var ug = Number(mqttData["solarManager/ug"]); - var evsoc = Number(mqttData["solarManager/evSOC"]); - var evlock = Number(mqttData["solarManager/evLock"]); - var evMode = mqttData["solarManager/evMode"]; - var evPower = Number(mqttData["solarManager/evPower"]); - var plugev = Number(mqttData["solarManager/evPlug"]); - var fuelev = Number(mqttData["solarManager/evFuel"]); - var heatOG = Number(mqttData["solarManager/heatOG"]); - var heatEG = Number(mqttData["solarManager/heatEG"]); - var puffO = Number(mqttData["solarManager/t_buffT"]); - var puffM = Number(mqttData["solarManager/t_buffM"]); - var puffU = Number(mqttData["solarManager/t_buffB"]); - var heatMode = mqttData["solarManager/heatMode"]; - var carRemChrg = Number(mqttData["solarManager/carRemChrg"]); - var aut = Number(mqttData["solarManager/autarky"]); - var Pheat = Number(mqttData["solarManager/pHeat"]); - var waterHeight = Number(mqttData["solarManager/waterHeight"]); - var waterTemp = Number(mqttData["solarManager/waterTemp"]); - var p_wr = Number(mqttData["solarManager/P_WR"]); - var eff = Number(mqttData["solarManager/eff"]); - var i_l1evu = Number(mqttData["solarManager/i_l1evu"]); - var i_l2evu = Number(mqttData["solarManager/i_l2evu"]); - var i_l3evu = Number(mqttData["solarManager/i_l3evu"]); - var evPowerOG = Number(mqttData["solarManager/evPowerOG"]); - var evPlugOG = Number(mqttData["solarManager/evPlugOG"]); - var evModeOG = mqttData["solarManager/evModeOG"]; - - - var common = -cons - ug - eg + og ; - if(common < 0){ - common = 0; - } - - setAttr('consumerArc', "d", describeArc(100, 100, 95, 0, Math.round(-cons / angleFactor))); - - setAttr("ogArc", "d", describeArc(100, 100, 95, 0, Math.round(-og / angleFactor))); - setText("consumerText", powerToString(-cons)); - - setText("consumerTextAllg", "Gemein: "+powerToString(common)); - - if (evPower > 0) { - setDisplay("evCharge", ""); - if (carRemChrg > -1) { - var today = new Date(); - today.setMinutes(today.getMinutes() + carRemChrg); - var h = today.getHours(); - var m = today.getMinutes(); - if (h < 10) - h = "0" + h; - if (m < 10) - m = "0" + m; - setText("evRemText", h + ":" + m); - } - } else { - setText("evRemText", ""); - } - setText("ogText", powerToString(-og)); - setText("egText", powerToString(eg)); - setText("ugText", powerToString(ug)); - setText("pvText", powerToString(pv)); - setText("pv1txt", powerToString(pv1)); - setText("pv2txt", powerToString(pv2)); - setText("pv3txt", powerToString(pv3)); - setText("il1txt", Math.round(i_l1evu * 10) / 10 + " A"); - setText("il2txt", Math.round(i_l2evu * 10) / 10 + " A"); - setText("il3txt", Math.round(i_l3evu * 10) / 10 + " A"); - setText("batText", powerToString(pbatt)); - setText("heatText", powerToString(Pheat)); - setText("evText", powerToString(evPower * 1000)); - setText("evTextOG", powerToString(evPowerOG * 1000)); - setText("genText", "Autarkie: " + Math.round(aut) + " %"); - setText("genInfoText", "Wirkungsgrad: " + Math.round(eff) + " %");// powerToString(p_wr); - setText("gridText", powerToString(grid)); - - for (i = 0; i < pvn.length; i++) { - setText("det_pv" + i + "P", powerToString(pvn[i])); - } - - tttext = "" - var i = 0; - while (mqttData["solarManager/inverters" + i]) { - if (mqttData["solarManager/inverters" + i+"/error"] > 0) { - tttext = tttext + '' + mqttData["solarManager/inverters" + i+"/name"] + ":" - tttext = tttext + '' + "--" + " °C" - tttext = tttext + '' + "-- W" + "" - } else { - tttext = tttext + '' + mqttData["solarManager/inverters" + i+"/name"] + ":" - tttext = tttext + '' + Math.round(mqttData["solarManager/inverters" + i+"/temp"]) + " °C" - tttext = tttext + '' + powerToString(mqttData["solarManager/inverters" + i+"/p_AC"]) + "" - } - i++; - } - - setHTML("invList", tttext); - setAttr("egArc", "d", describeArc(100, 100, 95, 0, Math.round(eg / angleFactor))); - setAttr("ugArc", "d", describeArc(100, 100, 95, 0, Math.round(ug / angleFactor))); - setAttr("pvArc", "d", describeArc(100, 100, 95, 0, Math.round(pv1 / angleFactor))); - setAttr("pv2Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor))); - setAttr("pv3Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor) + Math.round(pv3 / angleFactor))); - - if (pbatt < 0) { - if (limbatt < 100) { - setDisplay("charge", "none"); - setDisplay("batLim", ""); - setText("batLimText", Math.round(limbatt) + "%"); - } else { - setDisplay("charge", ""); - setDisplay("batLim", "none"); - } - setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(-pbatt / angleFactor))); - setAttr("battani", "class", "stream-rev"); - setFlowWidth("battani", 55 * (1 - Math.exp(pbatt / 2000))); - } else { - setDisplay("charge", "none"); - setDisplay("batLim", "none"); - setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(pbatt / angleFactor))); - setAttr("battani", "class", "stream"); - setFlowWidth("battani", 55 * (1 - Math.exp(-pbatt / 2000))); - } - setAttr("batChargeState", "y", (100 - soc) + "%"); - //setAttr("heatChargeState", "y", 100 - ((((puffO + puffM) / 2) - 30) * 1.66) + "%"); - setAttr('tmpH', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffO)); - setAttr('tmpM', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffM)); - setAttr('tmpL', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffU)); - setText("batSOC", soc + " %"); - setAttr("evArc", "d", describeArc(100, 100, 95, 0, Math.round(evPower * 2000 / angleFactor))); - setAttr("evChargeState", "y", (100 - evsoc) + "%"); - setText("evSOC", evsoc + " %"); - setText("evFuel", fuelev + " %"); - - setAttr("heatArc", "d", describeArc(100, 100, 95, 0, Math.round(Pheat / sixkWangleFactor))); - setText("heatUp", Math.round(puffO * 10) / 10 + " °C"); - setText("heatDown", Math.round(puffM * 10) / 10 + " °C"); - - if (evlock == false) { - setDisplay("evLock", "none"); - } else { - setDisplay("evLock", ""); - } - if (heatMode == "eco") { - setDisplay("heatEco", ""); - setDisplay("heatDefault", "none"); - } else { - setDisplay("heatEco", "none"); - setDisplay("heatDefault", ""); - } - if (evMode == "Eco") { - setDisplay("evEco", ""); - setDisplay("evNextTrip", "none"); - setDisplay("evDefault", "none"); - } else if (evMode == "Next Trip") { - setDisplay("evEco", "none"); - setDisplay("evNextTrip", ""); - setDisplay("evDefault", "none"); - } else { - setDisplay("evEco", "none"); - setDisplay("evNextTrip", "none"); - setDisplay("evDefault", ""); - } - if (heatOG) { - setDisplay("heatOG", ""); - } else { - setDisplay("heatOG", "none"); - } - if (heatEG) { - setDisplay("heatEG", ""); - } else { - setDisplay("heatEG", "none"); - } - if (plugev != "no car") { - setDisplay("evPlug", ""); - } else { - setDisplay("evPlug", "none"); - } - if (evModeOG == "Eco") { - setDisplay("evEcoOG", ""); - setDisplay("evNextTripOG", "none"); - setDisplay("evDefaultOG", "none"); - } else if (evModeOG == "Next Trip") { - setDisplay("evEcoOG", "none"); - setDisplay("evNextTripOG", ""); - setDisplay("evDefaultOG", "none"); - } else { - setDisplay("evEcoOG", "none"); - setDisplay("evNextTripOG", "none"); - setDisplay("evDefaultOG", ""); - } - if (evPlugOG > 0) { - setDisplay("evPlugOG", ""); - } else { - setDisplay("evPlugOG", "none"); - } - if (grid < 0) { - setAttr("gridArc", "d", describeArc(100, 100, 95, 0, -grid / angleFactor)); - setAttr("consGridani", "class", "stream-rev"); - setFlowWidth("consGridani", 55 * (1 - Math.exp(grid / 2000))); - } else { - setAttr("gridArc", "d", describeArc(100, 100, 95, 0, grid / angleFactor)); - setAttr("consGridani", "class", "stream"); - setFlowWidth("consGridani", 55 * (1 - Math.exp(-grid / 2000))); - } - - setFlowWidth("consEVani", 55 * (1 - Math.exp(-evPower))); - setFlowWidth("consEVOGani", 55 * (1 - Math.exp(-evPowerOG))); - setFlowWidth("consOGani", 55 * (1 - Math.exp(og / 2000))); - - setFlowWidth("consUGani", 55 * (1 - Math.exp( -ug / 2000))); - - setFlowWidth("consHeatAni", 55 * (1 - Math.exp(-Pheat / 2000))); - setFlowWidth("consAni", 55 * (1 - Math.exp(cons / 2000))); - setFlowWidth("consEGani", 55 * (1 - Math.exp(-eg / 2000))); - setFlowWidth("pvani", 55 * (1 - Math.exp(-pv / 2000))); -} - -} - -var chartSettings = { - type: 'line', - options: { - animation: true, - plugins: { - annotation: { - common: { type: 'box', drawTime: 'beforeDatasetsDraw', yScaleID: 'y-axis-0', backgroundColor: 'rgba(255, 255, 255, 0.05)', init: true }, - annotations: [] - }, - tooltip: { - position: 'nearest', - pointStyle: "circle", - boxWidth: 4, - usePointStyle: true, - callbacks: { - footer: function (tooltipItems){return ""}, - }, - }, - legend: { - position: "bottom", - labels: { - pointStyleWidth: 10, - usePointStyle: true, - pointStyle: "line", - } - }, - }, - responsive: true, - maintainAspectRatio: false, - interaction: { - intersect: false, - mode: 'index', - }, - scales: { - x: { - adapters: { - date: { - locale: "DE-de" - } - }, - ticks: { - - }, - type: 'timestack', - }, - y: { - stacked: true, - display: true, - min: 0, - position: 'left', - ticks: { - callback: value => `${value / 1000} kW`, - }, - /*title: { - display: true, - text: "Leistung" - }*/ - }, - y1: { - stacked: false, - display: true, - position: 'right', - min: 0, - max: 100, - ticks: { - callback: value => `${value} %`, - }, - /*title: { - display: true, - text: "Ladestand" - },*/ - data:{} - } - } - } - }; - - var forecastChartSettings = { - type: 'bar', - options: { - animation: true, - plugins: { - tooltip: { - position: 'nearest', - pointStyle: "circle", - boxWidth: 4, - usePointStyle: true, - callbacks: { - footer: function (tooltipItems){return ""}, - }, - }, - legend: { - position: "bottom", - labels: { - pointStyleWidth: 10, - usePointStyle: true, - pointStyle: "line", - } - }, - }, - responsive: true, - maintainAspectRatio: false, - interaction: { - intersect: false, - mode: 'index', - }, - scales: { - x: { - adapters: { - date: { - locale: "DE-de" - } - }, - ticks: { - }, - type: 'timestack', - timestack:{ - right_floating_tick_thres: 0.3, - format_style: {month: 'long'}, - } - }, - y: { - stacked: false, - display: true, - position: 'left', - ticks: { - callback: value => `${value / 1000} kW`, - }, - /*title: { - display: true, - text: "Leistung" - }*/ - }, - } - } - }; - - var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), { - keyboard: false - }); - var chartData = {}; - var mqttData = {}; - var timeFrom = -24; - var timeTo = 12; - const dayRange = () => ({ FROM: timeFrom, TO: timeTo }); - const consChart = new Chart( - document.querySelector('#consumption-chart'), - Object.assign({}, chartSettings) - ); - const prodChart = new Chart( - document.querySelector('#production-chart'), - Object.assign({}, chartSettings) - ); - - const foreChart = new Chart( - document.querySelector('#forecast-chart'), - Object.assign({}, forecastChartSettings) - ); - - function updateCharts(){ - getData(consChart, 'ajax/getConsData.php', { params: dayRange, sunrise: true, autoUpdate: false, updateMode: "none" }); - getData(prodChart, 'ajax/getProdData.php', { params: dayRange, sunrise: true, autoUpdate: false, updateMode: "none" }); - } - - function prevDay(){ - timeFrom -= 24; - timeTo -= 24; - - var now = new Date(); - now.setDate(now.getDate()+1+(timeFrom/24)); - var day = ("0" + now.getDate()).slice(-2); - var month = ("0" + (now.getMonth() + 1)).slice(-2); - var selected = now.getFullYear()+"-"+(month)+"-"+(day); - document.getElementById("DatePickerCons").value = selected; - document.getElementById("DatePickerProd").value = selected; - updateCharts(); - } - - function changeDay(e){ - const date1 = new Date(e.target.value); - const date2 = new Date(); - const difference = Math.floor((date1.getTime() - date2.getTime()) / (1000*60*60*24)) *24; - timeFrom = difference; - timeTo = difference+36; - updateCharts(); - //.then((ret) => enableEvents()); - } - - function nextDay(){ - timeFrom += 24; - timeTo += 24; - var now = new Date(); - now.setDate(now.getDate()+1+(timeFrom/24)); - var day = ("0" + now.getDate()).slice(-2); - var month = ("0" + (now.getMonth() + 1)).slice(-2); - var selected = now.getFullYear()+"-"+(month)+"-"+(day); - document.getElementById("DatePickerCons").value = selected; - document.getElementById("DatePickerProd").value = selected; - updateCharts(); - } - - document.addEventListener('readystatechange', function () { - if (event.target.readyState === "complete") { - solarMQTT.getMQTT(); - getData(consChart, 'ajax/getConsData.php', { params: dayRange, sunrise: true, updateMode: "none" }); - getData(prodChart, 'ajax/getProdData.php', { params: dayRange, sunrise: true, updateMode: "none" }); - getData(foreChart, 'ajax/getForecastData.php', { params: dayRange, updateMode: "none" }); - getStats("Stats-Year","ajax/getStats.php?type=ThisYear"); - solarSVG.fillElementArray(); - mountMeteogram(); - } - }); - - - - // Setzt den Schieberegler aus den Wattstunden, die die Wallbox meldet. - function setzeLadeRegler(wattstunden) { - const regler = document.getElementById("modal-slider"); - const whProProzent = Number(regler.dataset.whProProzent); - if (whProProzent > 0) { - regler.value = Math.round(Number(String(wattstunden)) / whProProzent); - } - } - - function openModal(type) { - document.getElementById("modalSaveBtn").style.display = type === "watering" ? "none" : ""; - switch(type){ - case "CarEG": - document.getElementById("modal-title").innerHTML = "Autoladung EG"; - contentURL = "ajax/carEG.php"; - break; - case "CarOG": - document.getElementById("modal-title").innerHTML = "Autoladung OG"; - contentURL = "ajax/carOG.php"; - break; - case "heater": - document.getElementById("modal-title").innerHTML = "Steuerung Heizstab"; - contentURL = "ajax/heater.php"; - break; - case "watering": - document.getElementById("modal-title").innerHTML = "Gartenbewässerung"; - contentURL = window.location.pathname.replace(/\/index\.php$/, "").replace(/\/$/, "") + "/ajax/watering.php"; - break; - default: - document.getElementById("modal-title").innerHTML = "Fehler"; - contentURL = ""; - break; - } - - modalBodyElement = document.getElementById('modal-body'); - modalBodyElement.innerHTML = loadingHTML("Wird geladen..."); - document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax); - document.getElementById("modalSaveBtn").contentURL = contentURL; - modalEV.show(); - fetch(contentURL, { - method: 'GET', - headers: { - 'X-Requested-From-Modal': 'a', - 'Requested-With-Ajax': 'ajax' - } - }) - .then(response => response.text()) - .then(html => { - modalBodyElement.innerHTML = html; - - var slider = document.getElementById("modal-slider"); - var span = document.getElementById("modal-slider-label"); - switch(type){ - case "CarEG": - bindModalSlider(slider, span, value => value + "%"); - document.getElementById("evStart/Stop").addEventListener("click", submitFormAjax); - document.getElementById("evStart/Stop").contentURL = contentURL; - mode = new TextDecoder().decode(mqttData["wattpilot/properties/lmo/state"]); - time = String(mqttData["wattpilot/properties/ftt/state"]); - energy = mqttData["wattpilot/properties/fte/state"]; - amp = new TextDecoder().decode(mqttData["wattpilot/properties/amp/state"]); - car = new TextDecoder().decode(mqttData["wattpilot/properties/car/state"]); - charge = Number(mqttData["solarManager/p_l1ev"])+Number(mqttData["solarManager/p_l2ev"])+Number(mqttData["solarManager/p_l3ev"]) - if (charge < 0.2) { document.getElementById("evStart/Stop").innerHTML = 'Laden starten' } - else { document.getElementById("evStart/Stop").innerHTML = 'Laden stoppen' } - if (mode == "Awattar") { document.getElementById("EV_Eco").click(); } - else if (mode == "Default") { document.getElementById("EV_Default").click(); } - else if (mode == "AutomaticStop") { document.getElementById("EV_NextTrip").click(); } - else { alert(mode); } - document.getElementById("EV_NextTripTime").value = time.toHHMM(); - // Wattstunden je Prozent liefert das Formular mit (carForm.php); - // die Akkugroesse steht in restricted/config.php. Frueher standen - // hier feste 14000 Wh - fuer beide Autos dasselbe, und fuer keines - // von beiden richtig. - setzeLadeRegler(energy); - document.getElementById("modal-slider").dispatchEvent(new Event('input')); - if (amp == "16") { document.getElementById("EV_16A").click(); } - else if (amp == "10") { document.getElementById("EV_10A").click(); } - else if (amp == "6") { document.getElementById("EV_6A").click(); } - break; - case "CarOG": - bindModalSlider(slider, span, value => value + "%"); - document.getElementById("evStart/Stop").addEventListener("click", submitFormAjax); - document.getElementById("evStart/Stop").contentURL = contentURL; - mode = mqttData["go-eCharger/270003/lmo"]; - time = String(mqttData["go-eCharger/270003/att"]); - energy = mqttData["go-eCharger/270003/ate"]; - amp = mqttData["go-eCharger/270003/amp"]; - car = mqttData["go-eCharger/270003/car"]; - if (car != "2") { document.getElementById("evStart/Stop").innerHTML = 'Laden starten' } - else { document.getElementById("evStart/Stop").innerHTML = 'Laden stoppen' } - if (mode == "4") { document.getElementById("EV_Eco").click(); } - else if (mode == "3") { document.getElementById("EV_Default").click(); } - else if (mode == "5") { document.getElementById("EV_NextTrip").click(); } - else { alert(mode); } - document.getElementById("EV_NextTripTime").value = time.toHHMM(); - // Wattstunden je Prozent liefert das Formular mit (carForm.php); - // die Akkugroesse steht in restricted/config.php. Frueher standen - // hier feste 14000 Wh - fuer beide Autos dasselbe, und fuer keines - // von beiden richtig. - setzeLadeRegler(energy); - document.getElementById("modal-slider").dispatchEvent(new Event('input')); - if (amp == "16") { document.getElementById("EV_16A").click(); } - else if (amp == "10") { document.getElementById("EV_10A").click(); } - else if (amp == "6") { document.getElementById("EV_6A").click(); } - break; - case "heater": - bindModalSlider(slider, span, value => value == 0 ? "Automatik" : (value / 10) + " kW"); - if(mqttData["solarManager/heatMode"] == "eco"){ - document.getElementById("modal-slider").value = 0; - }else{ - document.getElementById("modal-slider").value = Math.round(mqttData["solarManager/pHeat"]/100); - } - document.getElementById("modal-slider").dispatchEvent(new Event('input')); - break; - case "watering": - initializeWateringModal(); - break; - } - - }) - .catch(error => { - modalBodyElement.innerHTML = error.message; - }); - } - - const wateringTimersLastSeen = {}; - // Eine Tabelle fuer alles: Ventil, Automatik-Zone des Skripts, Beschriftung - // und die Anzeige-Ids im Realtime-SVG. Vorher standen dieselben Zuordnungen - // in drei getrennten Tabellen. - const wateringZones = { - troughs: { - topic: "Gartenwasser/vorn", valve: 1, auto: "troege", label: "Tröge", - statusId: "wateringTroughStatus", iconId: "wateringTroughIcon" - }, - frontGarden: { - topic: "Gartenwasser/vorn", valve: 6, auto: "garten_vorn", label: "Vorn", - statusId: "wateringFrontStatus", iconId: "wateringFrontIcon" - }, - raisedBeds: { - topic: "Gartenwasser/hinten", valve: 6, auto: "hochbeete", label: "Beete", - statusId: "wateringBedsStatus", iconId: "wateringBedsIcon" - } - }; - // Feste Liste - der Sekundentakt soll sie nicht jedes Mal neu aufbauen. - const wateringZoneList = Object.entries(wateringZones); - - function wateringCommand(topic, command, directTopic = false) { - const feedback = document.getElementById("watering-feedback"); - const targetTopic = directTopic ? topic : topic + "/set"; - const payload = typeof command === "string" ? command : JSON.stringify(command); - const endpoint = window.location.pathname.replace(/\/index\.php$/, "").replace(/\/$/, "") + "/ajax/watering.php"; - const showFeedback = (message, type) => { - if (!feedback) return; - feedback.textContent = message; - feedback.className = "alert alert-" + type + " mb-3"; - feedback.style.whiteSpace = "pre-line"; - }; - fetch(endpoint, { - method: "POST", - credentials: "same-origin", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ topic: targetTopic, payload: payload }) - }).then(async (response) => { - const result = await response.json(); - if (!response.ok || !result.success) throw new Error(result.message || "Unbekannter MQTT-Fehler."); - showFeedback("Befehl gesendet.\nTopic: " + result.topic + "\nInhalt: " + result.payload, "success"); - }).catch((error) => { - showFeedback("Befehl konnte nicht gesendet werden: " + error.message + "\nTopic: " + targetTopic + "\nInhalt: " + payload, "danger"); - }); - } - - function initializeWateringModal() { - document.querySelectorAll("[data-watering-toggle]").forEach((button) => { - const updateButton = () => { - const zone = wateringZones[button.dataset.wateringZone]; - setzeZonenButton(button, wateringLabel(zone.topic, zone.valve)); - }; - button.addEventListener("click", () => { - const zone = wateringZones[button.dataset.wateringZone]; - if (wateringLabel(zone.topic, zone.valve) === "aus") { - const timerInput = document.querySelector(`[data-watering-timer='${button.dataset.wateringZone}']`); - const timer = Math.min(120, Math.max(1, Number(timerInput.value) || 30)); - wateringCommand(zone.topic, { valve: zone.valve, set: "open", timer: timer }); - } else { - wateringCommand(zone.topic, { valve: zone.valve, set: "close" }); - } - }); - updateButton(); - }); - document.querySelectorAll("[data-watering-auto-toggle]").forEach((button) => { - button.addEventListener("click", () => { - const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(","); - const modes = button.dataset.wateringAuto.split(","); - const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index])); - topics.forEach((topic, index) => { - wateringCommand(topic + "/auto", active ? "Stop" : modes[index], true); - }); - }); - updateWateringAutoButton(button); - }); - updateWateringStatus(); - } - - // Die Anzeige laeuft im Sekundentakt, die Nutzlasten aendern sich aber nur, - // wenn eine neue MQTT-Nachricht kommt. messageReceived legt dabei jedes Mal - // ein neues Buffer-Objekt ab - der Identitaetsvergleich reicht also als - // Gueltigkeitspruefung, ein Invalidieren von Hand braucht es nicht. - const wateringJsonCache = {}; - const wateringDecoder = new TextDecoder(); - - function wateringJson(topic) { - const value = mqttData[topic]; - if (!value) return null; - const cached = wateringJsonCache[topic]; - if (cached && cached.raw === value) return cached.parsed; - - let parsed = null; - try { - parsed = JSON.parse(typeof value === "string" ? value : wateringDecoder.decode(value)); - } catch (error) { - parsed = null; - } - wateringJsonCache[topic] = { raw: value, parsed: parsed }; - return parsed; - } - - function wateringHistory(topic, valve) { - const data = wateringJson(topic + "/LastWatering"); - if (!data) return { dateTime: "--", duration: "--" }; - return { - dateTime: data["wateringDateTime" + valve] || "--", - duration: data["wateringDurationSecs" + valve] !== undefined - ? wateringTimeLabel(Number(data["wateringDurationSecs" + valve])) - : "--" - }; - } - - // Tagesdatum im Format der Ventilsteuerung ("29.08.26"). - function wateringDayStamp(date) { - const zwei = (zahl) => String(zahl).padStart(2, "0"); - return zwei(date.getDate()) + "." + zwei(date.getMonth() + 1) + "." + zwei(date.getFullYear() % 100); - } - - // Tagessummen der Ventilsteuerung: WateringToday fuer den laufenden Tag, - // LastWateringDay fuer den letzten Tag, an dem ueberhaupt bewaessert wurde. - // Beide Nutzlasten tragen den zugehoerigen Tag als "wateringDay" mit. Bei - // WateringToday wird der geprueft - die Nachricht ist retained, nach einem - // Tageswechsel ohne Bewaesserung stuende sonst die Summe von gestern als - // "heute" in der Karte. - function wateringDayTotal(topic, subTopic, feld, valve, mussHeuteSein) { - const data = wateringJson(topic + "/" + subTopic); - if (!data) return { label: "--", day: "" }; - const tag = data.wateringDay || ""; - const sekunden = data[feld + valve]; - if (sekunden === undefined) return { label: "--", day: tag }; - if (mussHeuteSein && tag !== wateringDayStamp(new Date())) return { label: "--", day: tag }; - return { label: wateringTimeLabel(Number(sekunden)), day: tag }; - } - - function wateringAutoActive(topic, mode) { - const timers = wateringJson(topic + "/Timers"); - return !!(timers && timers.auto === mode); - } - - function wateringValue(data, valve) { - if (!data) return null; - const candidates = [ - data.timers && data.timers[valve], - data.timers && data.timers[String(valve)], - data[valve], - data[String(valve)], - data.valves && data.valves[valve], - data.valves && data.valves[String(valve)], - data.status && data.status[valve], - data.status && data.status[String(valve)], - data.valveState && data.valveState[valve], - data.valveState && data.valveState[String(valve)], - data.setValveState && data.setValveState[valve], - data.setValveState && data.setValveState[String(valve)] - ]; - for (const candidate of candidates) { - if (candidate !== undefined) return candidate; - } - return null; - } - - function wateringState(value) { - if (value && typeof value === "object") { - return value.state ?? value.status ?? value.valveState ?? value.set; - } - return value; - } - - function wateringSeconds(value) { - if (value && typeof value === "object") { - value = value.remaining ?? value.timer ?? value.seconds ?? value.time; - } - return Number(value); - } - - function wateringLabel(topic, valve) { - const timers = wateringJson(topic + "/Timers"); - const status = wateringJson(topic + "/status"); - const lastSeen = wateringTimersLastSeen[topic + "/Timers"]; - if (!lastSeen || Date.now() - lastSeen > 3000) return "aus"; - const remaining = wateringValue(timers, valve); - if (remaining !== null) { - const seconds = wateringSeconds(remaining); - if (seconds !== 0) return wateringTimeLabel(Math.abs(seconds)); - return "aus"; - } - const state = wateringState(wateringValue(status, valve)); - if (state === true || state === 1 || state === "1" || state === "open" || state === "OPEN") return "an"; - if (state === false || state === 0 || state === "0" || state === "close" || state === "closed" || state === "CLOSED") return "aus"; - return "--"; - } - - function wateringTimeLabel(seconds) { - if (seconds < 60) return seconds + "s"; - return Math.ceil(seconds / 60) + "m"; - } - - function wateringIsPaused(topic, valve) { - const timers = wateringJson(topic + "/Timers"); - const remaining = wateringValue(timers, valve); - return remaining !== null && wateringSeconds(remaining) < 0; - } - - // Liest den von auto_watering.py stündlich veröffentlichten Regen-Status - // (retained, siehe Gartenwasser/AutoWatering//RainStatus) und sagt, - // ob für die Zone heute noch eine automatische Bewässerung aussteht. - function wateringAutoPlanned(autoZoneKey) { - const status = wateringJson("Gartenwasser/AutoWatering/" + autoZoneKey + "/RainStatus"); - return !!(status && status.would_water && !status.watered_recently); - } - - // Zeitraeume und Regenmengen in Klartext. "2 T." war zu kryptisch. - function wateringTage(tage) { - const zahl = Number(tage); - const text = Number.isInteger(zahl) ? String(zahl) : String(zahl).replace(".", ","); - return zahl === 1 ? text + " Tag" : text + " Tagen"; - } - - function wateringMm(wert) { - return String(wert).replace(".", ",") + " mm"; - } - - // Sagt in einem Satz, was die Automatik fuer die Zone vorhat und warum. - // Speist sich aus derselben RainStatus-Nachricht wie die Anzeige im SVG. - // Der Titel nennt zusaetzlich die Regel, nach der entschieden wird - die - // Schwellenwerte unterscheiden sich je Zone deutlich. - function wateringPlanText(autoZoneKey) { - const status = wateringJson("Gartenwasser/AutoWatering/" + autoZoneKey + "/RainStatus"); - if (!status) return { text: "keine Planungsdaten", titel: "" }; - - const regel = []; - if (status.threshold_mm !== undefined && status.threshold_days !== undefined) { - regel.push("Bewässert wird, wenn in " + wateringTage(status.threshold_days) - + " weniger als " + wateringMm(status.threshold_mm) + " Regen fallen."); - } - if (status.max_dry_days) { - regel.push("Ebenso, wenn seit " + wateringTage(status.max_dry_days) + " nicht bewässert wurde."); - } - const titel = regel.join(" "); - - if (status.watered_recently) { - const zeitpunkt = status.last_watering - ? " – zuletzt " + String(status.last_watering).replace(/[.]\d{4} /, ". ") - : ""; - return { text: "erledigt" + zeitpunkt, titel: titel }; - } - const regen = status.rain_mm !== undefined && status.threshold_days !== undefined - ? wateringMm(status.rain_mm) + " Regen in " + wateringTage(status.threshold_days) - : ""; - if (!status.would_water) { - return { text: "kein Bedarf" + (regen ? " – " + regen : ""), titel: titel }; - } - if (status.forced_dry) { - const trocken = status.max_dry_days - ? " – seit " + wateringTage(status.max_dry_days) + " nicht bewässert" - : " – lange nicht bewässert"; - return { text: "heute geplant" + trocken, titel: titel }; - } - return { text: "heute geplant" + (regen ? " – nur " + regen : ""), titel: titel }; - } - - // Alle Zonen stammen aus demselben Lauf des Skripts - der erste gefundene - // Zeitstempel steht daher fuer alle. Die Sekunden schneidet slice(0, 16) ab. - function wateringPlanUpdated() { - for (const [, zone] of wateringZoneList) { - const status = wateringJson("Gartenwasser/AutoWatering/" + zone.auto + "/RainStatus"); - if (status && status.updated) return "Planung vom " + String(status.updated).slice(0, 16); - } - return ""; - } - - function updateWateringStatus() { - wateringZoneList.forEach(([key, zone]) => { - const state = wateringLabel(zone.topic, zone.valve); - const paused = wateringIsPaused(zone.topic, zone.valve); - const planned = state === "aus" && wateringAutoPlanned(zone.auto); - - // Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement. - const status = svgElement(zone.statusId); - const icon = svgElement(zone.iconId); - if (status && icon) updateWateringIndicator(status, icon, zone.label, state, paused, planned); - - // Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen. - setzeZonenButton(document.querySelector(`[data-watering-toggle][data-watering-zone='${key}']`), state); - setzeSchild(`[data-watering-state='${key}']`, state, paused, planned); - - const history = wateringHistory(zone.topic, zone.valve); - setzeZelle(`[data-watering-last='${key}']`, history.dateTime); - setzeZelle(`[data-watering-duration='${key}']`, history.duration); - const heute = wateringDayTotal(zone.topic, "WateringToday", "wateringDurationTodaySecs", zone.valve, true); - const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false); - setzeZelle(`[data-watering-today='${key}']`, heute.label, heute.day && "Stand: " + heute.day); - setzeZelle(`[data-watering-prevday='${key}']`, vortag.label, vortag.day && "Bewässerungstag: " + vortag.day); - const plan = wateringPlanText(zone.auto); - setzeZelle(`[data-watering-plan='${key}']`, plan.text, plan.titel); - }); - document.querySelectorAll("[data-watering-auto-toggle]").forEach(updateWateringAutoButton); - setzeZelle("[data-watering-plan-updated]", wateringPlanUpdated()); - } - - // Der Knopf zeigt die naechste Aktion, nicht den Zustand: laeuft die Zone, - // steht "Aus" darauf. - function setzeZonenButton(button, state) { - if (!button) return; - const beschriftung = state === "aus" ? "Ein" : "Aus"; - if (button.textContent !== beschriftung) button.textContent = beschriftung; - button.classList.toggle("btn-success", state === "aus"); - button.classList.toggle("btn-danger", state !== "aus"); - } - - // Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen, - // pausiert gelb, von der Automatik geplant blau, sonst grau. - function setzeSchild(selektor, state, paused, planned) { - const schild = document.querySelector(selektor); - if (!schild) return; - let text = "unbekannt"; - let farbe = "bg-secondary"; - if (state !== "--" && state !== "aus") { - text = paused ? "pausiert " + state : "läuft " + state; - farbe = paused ? "bg-warning text-dark" : "bg-success"; - } else if (state === "aus") { - text = planned ? "heute geplant" : "aus"; - farbe = planned ? "bg-primary" : "bg-secondary"; - } - if (schild.textContent !== text) schild.textContent = text; - const klasse = "badge " + farbe; - if (schild.className !== klasse) schild.className = klasse; - } - - // Schreibt nur bei echter Aenderung - die Tabelle wird im Sekundentakt - // aktualisiert, ihre Werte aendern sich aber hoechstens minuetlich. - function setzeZelle(selektor, text, titel) { - const element = document.querySelector(selektor); - if (!element) return; - if (element.textContent !== text) element.textContent = text; - if (titel) { - if (element.getAttribute("title") !== titel) element.setAttribute("title", titel); - } else if (element.hasAttribute("title")) { - element.removeAttribute("title"); - } - } - - function updateWateringAutoButton(button) { - const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(","); - const modes = button.dataset.wateringAuto.split(","); - const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index])); - if (!button.dataset.wateringLabel) { - button.dataset.wateringLabel = button.textContent.replace(/^Automatik\s+/, ""); - } - button.textContent = (active ? "Aus: " : "Ein: ") + button.dataset.wateringLabel; - button.classList.toggle("btn-outline-primary", !active); - button.classList.toggle("btn-outline-danger", active); - } - - function updateWateringIndicator(status, icon, name, state, paused, planned) { - // "Aus" bleibt bei nicht geplant stehen; bei geplant ersetzt die - // Stoppuhr (U+23F1, ohne Emoji-Variationsselektor -> einfarbig, folgt - // der Icon-Farbe) den Text, um Platz auf der textPath zu sparen. - // Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung - // wuerde jede Sekunde derselbe Wert geschrieben und das Icon samt - // Filter neu gezeichnet. - const text = state === "aus" ? (planned ? "⏱" : "Aus") : state; - if (status.textContent !== text) status.textContent = text; - // Geplant-Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water"). - const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : planned ? "#3A6292" : "#6c757d"; - if (icon.getAttribute("color") !== color) { - icon.setAttribute("color", color); - icon.setAttribute("fill", color); - icon.style.color = color; - } - const titel = name + ": " + (state === "aus" && planned ? "Aus (Automatik heute noch geplant)" : state); - const titelKnoten = icon.querySelector("title"); - if (titelKnoten.textContent !== titel) titelKnoten.textContent = titel; - } - - window.setInterval(updateWateringStatus, 1000); - - - - - - - function polarToCartesian(centerX, centerY, radius, angleInDegrees) { - var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; - - return { - x: centerX + (radius * Math.cos(angleInRadians)), - y: centerY + (radius * Math.sin(angleInRadians)) - }; - } - - function getRBGComponent(colRange, minCol, valRange, minVal, val) { - return Math.round(((val - minVal) / valRange) * colRange + minCol) - .toString(16) - .toUpperCase() - .padStart(2, '0'); - } - - function assignColor(minCol, maxCol, minVal, maxVal, val) { - var color = ""; - var minR = parseInt(minCol.substring(1, 3), 16); - var maxR = parseInt(maxCol.substring(1, 3), 16); - var minG = parseInt(minCol.substring(3, 5), 16); - var maxG = parseInt(maxCol.substring(3, 5), 16); - var minB = parseInt(minCol.substring(5, 7), 16); - var maxB = parseInt(maxCol.substring(5, 7), 16); - var valsRange = maxVal - minVal; - var rangeG = maxG - minG; - var rangeR = maxR - minR; - var rangeB = maxB - minB; - if (val > maxVal) - val = maxVal; - else if (val < minVal) - val = minVal; - - color = '#' - + getRBGComponent(rangeR, minR, valsRange, minVal, val) - + getRBGComponent(rangeG, minG, valsRange, minVal, val) - + getRBGComponent(rangeB, minB, valsRange, minVal, val); - return color; - } - - function describeArc(x, y, radius, startAngle, endAngle) { - if (endAngle > 288) { - endAngle = 288; - } - // Auf volle Grad runden: bei Radius 95 ist ein Grad rund 1,7 Einheiten, - // feiner sieht niemand. Dafuer bleibt das d-Attribut bei kleinen - // Schwankungen unveraendert und setAttr unterdrueckt den Schreibvorgang - - // sonst laesst jeder Zappler den Bogen samt Filter neu zeichnen. - startAngle = Math.round(startAngle); - endAngle = Math.round(endAngle); - var start = polarToCartesian(x, y, radius, endAngle); - var end = polarToCartesian(x, y, radius, startAngle); - var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; - // zwei Nachkommastellen genuegen und halten das Attribut kurz - var rnd = (v) => Math.round(v * 100) / 100; - return [ - "M", rnd(start.x), rnd(start.y), - "A", radius, radius, 0, largeArcFlag, 0, rnd(end.x), rnd(end.y) - ].join(" "); - } +let solarMqttClient; +let solarSvgUpdatePending = false; + +const solarMQTT = { + getMQTT: function () { + const id = Math.random().toString(36).substring(7); + const topic = "#"; + const connection = "wss://mqtt.nas.el-wa.org:443" + mqttsolarTreeDone = false; + // const connection = "ws://username:password@37.97.203.138:8083" // Works + // const connection = "wss://public:public@public.cloud.shiftr.io" // Works + const client = mqtt.connect(connection, { + rejectUnauthorized: false, + }); + solarMqttClient = client; + + client.on("message", messageReceived); + client.on("connect", function () { + client.subscribe("solarManager/#"); + client.subscribe("wattpilot/properties/lmo/state"); + client.subscribe("wattpilot/properties/ftt/state"); + client.subscribe("wattpilot/properties/fte/state"); + client.subscribe("wattpilot/properties/amp/state"); + client.subscribe("wattpilot/properties/car/state"); + client.subscribe("go-eCharger/270003/amp"); + client.subscribe("go-eCharger/270003/ate"); + client.subscribe("go-eCharger/270003/lmo"); + client.subscribe("go-eCharger/270003/att"); + client.subscribe("go-eCharger/270003/car"); + client.subscribe("weatherStation/#"); + client.subscribe("Gartenwasser/vorn/status"); + client.subscribe("Gartenwasser/vorn/Timers"); + client.subscribe("Gartenwasser/vorn/LastWatering"); + client.subscribe("Gartenwasser/vorn/WateringToday"); + client.subscribe("Gartenwasser/vorn/LastWateringDay"); + client.subscribe("Gartenwasser/hinten/status"); + client.subscribe("Gartenwasser/hinten/Timers"); + client.subscribe("Gartenwasser/hinten/LastWatering"); + client.subscribe("Gartenwasser/hinten/WateringToday"); + client.subscribe("Gartenwasser/hinten/LastWateringDay"); + client.subscribe("Gartenwasser/AutoWatering/#"); + }); + client.on("error", function (error) { + //alert("MQTT Error: " + error); + }); + client.on('end', function () { + setTimeout(getMQTT, 5000); + alert("MQTT Disconnected, try to reconnect in 5 secs."); + }) + + function getNestedProp(obj, path) { + return path.split('/').reduce((acc, key) => acc && acc[key], obj); + } + function setNestedProp(obj, path, value) { + var schema = obj; // a moving reference to internal objects within obj + var pList = path.split('/'); + var len = pList.length; + for (var i = 0; i < len - 1; i++) { + var elem = pList[i]; + if (!schema[elem]) schema[elem] = {} + schema = schema[elem]; + } + + schema[pList[len - 1]] = value; + } + + function messageReceived(topic, message) { + mqttData[topic] = message; + if (topic === "Gartenwasser/vorn/Timers" || topic === "Gartenwasser/hinten/Timers") { + wateringTimersLastSeen[topic] = Date.now(); + } + if (topic.startsWith("Gartenwasser/") && (topic.endsWith("/status") || topic.endsWith("/Timers") || topic.endsWith("/LastWatering") || topic.endsWith("/RainStatus") + || topic.endsWith("/WateringToday") || topic.endsWith("/LastWateringDay"))) { + updateWateringStatus(); + } + if (topic == "solarManager/P_Load") { + if (!solarSvgUpdatePending) { + solarSvgUpdatePending = true; + setTimeout(function () { + solarSvgUpdatePending = false; + solarSVG.updateValuesMQTT(mqttData); + }, 1000); + } + }else if(topic == "weatherStation/windDeg"){ + setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation/" + name]) }, 200); //give the object tree some time to build up and receive all values + } + } + }, + publish: function (topic, payload, callback) { + if (!solarMqttClient || !solarMqttClient.connected) { + throw new Error("MQTT ist noch nicht verbunden."); + } + solarMqttClient.publish(topic, typeof payload === "string" ? payload : JSON.stringify(payload), callback); + } +} + +// Die Knoten im eingebetteten Realtime-SVG bleiben ueber die Lebensdauer der +// Seite bestehen. updateValuesMQTT fasst 104 davon an - bis zu einmal pro +// Sekunde -, deshalb werden sie einmal nachgeschlagen und dann behalten. +const svgElements = {}; +function svgElement(id) { + let node = svgElements[id]; + if (node === undefined) { + node = svgElements[id] = document.getElementById(id); + } + return node; +} + +// Nur schreiben, wenn sich der Wert wirklich geaendert hat. Jeder Schreibzugriff +// macht Style und Paint des Elements ungueltig - bei einem gefilterten Element +// wird dabei sein Filter neu gerastert. Gemessen waren rund 71 % der Zugriffe +// pro Update reine Wiederholungen desselben Werts; die entfallen damit. +const letzterWert = new Map(); +function schreibe(schluessel, wert, anwenden) { + const s = String(wert); + if (letzterWert.get(schluessel) === s) return; + letzterWert.set(schluessel, s); + anwenden(s); +} +// textContent statt innerHTML: setzt einen Textknoten, ohne den HTML-Parser zu bemuehen. +function setText(id, wert) { schreibe("t:" + id, wert, s => { svgElement(id).textContent = s; }); } +function setHTML(id, markup) { schreibe("h:" + id, markup, s => { svgElement(id).innerHTML = s; }); } +function setAttr(id, name, wert) { schreibe("a:" + id + ":" + name, wert, s => { svgElement(id).setAttribute(name, s); }); } +function setDisplay(id, wert) { schreibe("d:" + id, wert, s => { svgElement(id).style.display = s; }); } + + + +// Punktgroesse einer Flusslinie setzen und die Animation anhalten, wenn kein +// Fluss anliegt. --w ist der Durchmesser, die CSS-Regel macht daraus den Radius +// aller Punkte der Gruppe. will-change nur solange die Gruppe laeuft: es fordert +// eine eigene Compositor-Ebene an, und die kostet Speicher. +function setFlowWidth(id, width) { + const flow = svgElement(id); + // Eine negative Breite heisst "kein Fluss in dieser Richtung" - die + // Aufrufer rechnen 55*(1-exp(x)), was bei umgekehrtem Vorzeichen negativ + // wird. Frueher zeichnete eine negative stroke-width schlicht nichts. + // Math.max haelt das bei; Math.abs wuerde stattdessen einen Fluss in der + // falschen Richtung mit voller Staerke anzeigen. + const breite = Math.max(0, width); + const sichtbar = breite >= 0.5; + flow.style.setProperty("--w", breite + "px"); + flow.style.animationPlayState = sichtbar ? "running" : "paused"; + flow.style.willChange = sichtbar ? "transform" : "auto"; +} + +const solarSVG = { + updateCnt: 99, + /** + * Beide Zwischenspeicher verwerfen - noetig, falls das SVG neu gezeichnet + * wird. Nur die Knoten zu vergessen reicht nicht: letzterWert wuerde dann + * jeden Schreibzugriff als "unveraendert" ueberspringen und das frische + * SVG behielte seine Platzhalter. + */ + fillElementArray: function () { + for (const id in svgElements) delete svgElements[id]; + letzterWert.clear(); + }, + updateValuesMQTT: function (mqttData) { + + const angleFactor = 15000 / 288; + const sixkWangleFactor = 6000 / 288; + var cons = Number(mqttData["solarManager/P_Load"]); + var pvn = JSON.parse(mqttData["solarManager/P_PVn"]); + var pv = Number(mqttData["solarManager/P_PV"]); + var pv2 = Number(pvn[1]); + var pv1 = Number(pvn[0]); + var pv3 = pv - (pv1 + pv2); + var pbatt = Number(mqttData["solarManager/P_Akku"]); + var limbatt = Number(mqttData["solarManager/crgMaxPct"]); + var grid = Number(mqttData["solarManager/P_Grid"]); + var soc = Number(mqttData["solarManager/SOC"]); + var og = Number(mqttData["solarManager/og"]); + var eg = Number(mqttData["solarManager/eg"]); + var ug = Number(mqttData["solarManager/ug"]); + var evsoc = Number(mqttData["solarManager/evSOC"]); + var evlock = Number(mqttData["solarManager/evLock"]); + var evMode = mqttData["solarManager/evMode"]; + var evPower = Number(mqttData["solarManager/evPower"]); + var plugev = Number(mqttData["solarManager/evPlug"]); + var fuelev = Number(mqttData["solarManager/evFuel"]); + var heatOG = Number(mqttData["solarManager/heatOG"]); + var heatEG = Number(mqttData["solarManager/heatEG"]); + var puffO = Number(mqttData["solarManager/t_buffT"]); + var puffM = Number(mqttData["solarManager/t_buffM"]); + var puffU = Number(mqttData["solarManager/t_buffB"]); + var heatMode = mqttData["solarManager/heatMode"]; + var carRemChrg = Number(mqttData["solarManager/carRemChrg"]); + var aut = Number(mqttData["solarManager/autarky"]); + var Pheat = Number(mqttData["solarManager/pHeat"]); + var waterHeight = Number(mqttData["solarManager/waterHeight"]); + var waterTemp = Number(mqttData["solarManager/waterTemp"]); + var p_wr = Number(mqttData["solarManager/P_WR"]); + var eff = Number(mqttData["solarManager/eff"]); + var i_l1evu = Number(mqttData["solarManager/i_l1evu"]); + var i_l2evu = Number(mqttData["solarManager/i_l2evu"]); + var i_l3evu = Number(mqttData["solarManager/i_l3evu"]); + var evPowerOG = Number(mqttData["solarManager/evPowerOG"]); + var evPlugOG = Number(mqttData["solarManager/evPlugOG"]); + var evModeOG = mqttData["solarManager/evModeOG"]; + + + var common = -cons - ug - eg + og ; + if(common < 0){ + common = 0; + } + + setAttr('consumerArc', "d", describeArc(100, 100, 95, 0, Math.round(-cons / angleFactor))); + + setAttr("ogArc", "d", describeArc(100, 100, 95, 0, Math.round(-og / angleFactor))); + setText("consumerText", powerToString(-cons)); + + setText("consumerTextAllg", "Gemein: "+powerToString(common)); + + if (evPower > 0) { + setDisplay("evCharge", ""); + if (carRemChrg > -1) { + var today = new Date(); + today.setMinutes(today.getMinutes() + carRemChrg); + var h = today.getHours(); + var m = today.getMinutes(); + if (h < 10) + h = "0" + h; + if (m < 10) + m = "0" + m; + setText("evRemText", h + ":" + m); + } + } else { + setText("evRemText", ""); + } + setText("ogText", powerToString(-og)); + setText("egText", powerToString(eg)); + setText("ugText", powerToString(ug)); + setText("pvText", powerToString(pv)); + setText("pv1txt", powerToString(pv1)); + setText("pv2txt", powerToString(pv2)); + setText("pv3txt", powerToString(pv3)); + setText("il1txt", Math.round(i_l1evu * 10) / 10 + " A"); + setText("il2txt", Math.round(i_l2evu * 10) / 10 + " A"); + setText("il3txt", Math.round(i_l3evu * 10) / 10 + " A"); + setText("batText", powerToString(pbatt)); + setText("heatText", powerToString(Pheat)); + setText("evText", powerToString(evPower * 1000)); + setText("evTextOG", powerToString(evPowerOG * 1000)); + setText("genText", "Autarkie: " + Math.round(aut) + " %"); + setText("genInfoText", "Wirkungsgrad: " + Math.round(eff) + " %");// powerToString(p_wr); + setText("gridText", powerToString(grid)); + + for (i = 0; i < pvn.length; i++) { + setText("det_pv" + i + "P", powerToString(pvn[i])); + } + + tttext = "" + var i = 0; + while (mqttData["solarManager/inverters" + i]) { + if (mqttData["solarManager/inverters" + i+"/error"] > 0) { + tttext = tttext + '' + mqttData["solarManager/inverters" + i+"/name"] + ":" + tttext = tttext + '' + "--" + " °C" + tttext = tttext + '' + "-- W" + "" + } else { + tttext = tttext + '' + mqttData["solarManager/inverters" + i+"/name"] + ":" + tttext = tttext + '' + Math.round(mqttData["solarManager/inverters" + i+"/temp"]) + " °C" + tttext = tttext + '' + powerToString(mqttData["solarManager/inverters" + i+"/p_AC"]) + "" + } + i++; + } + + setHTML("invList", tttext); + setAttr("egArc", "d", describeArc(100, 100, 95, 0, Math.round(eg / angleFactor))); + setAttr("ugArc", "d", describeArc(100, 100, 95, 0, Math.round(ug / angleFactor))); + setAttr("pvArc", "d", describeArc(100, 100, 95, 0, Math.round(pv1 / angleFactor))); + setAttr("pv2Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor))); + setAttr("pv3Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor) + Math.round(pv3 / angleFactor))); + + if (pbatt < 0) { + if (limbatt < 100) { + setDisplay("charge", "none"); + setDisplay("batLim", ""); + setText("batLimText", Math.round(limbatt) + "%"); + } else { + setDisplay("charge", ""); + setDisplay("batLim", "none"); + } + setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(-pbatt / angleFactor))); + setAttr("battani", "class", "stream-rev"); + setFlowWidth("battani", 55 * (1 - Math.exp(pbatt / 2000))); + } else { + setDisplay("charge", "none"); + setDisplay("batLim", "none"); + setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(pbatt / angleFactor))); + setAttr("battani", "class", "stream"); + setFlowWidth("battani", 55 * (1 - Math.exp(-pbatt / 2000))); + } + setAttr("batChargeState", "y", (100 - soc) + "%"); + //setAttr("heatChargeState", "y", 100 - ((((puffO + puffM) / 2) - 30) * 1.66) + "%"); + setAttr('tmpH', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffO)); + setAttr('tmpM', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffM)); + setAttr('tmpL', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffU)); + setText("batSOC", soc + " %"); + setAttr("evArc", "d", describeArc(100, 100, 95, 0, Math.round(evPower * 2000 / angleFactor))); + setAttr("evChargeState", "y", (100 - evsoc) + "%"); + setText("evSOC", evsoc + " %"); + setText("evFuel", fuelev + " %"); + + setAttr("heatArc", "d", describeArc(100, 100, 95, 0, Math.round(Pheat / sixkWangleFactor))); + setText("heatUp", Math.round(puffO * 10) / 10 + " °C"); + setText("heatDown", Math.round(puffM * 10) / 10 + " °C"); + + if (evlock == false) { + setDisplay("evLock", "none"); + } else { + setDisplay("evLock", ""); + } + if (heatMode == "eco") { + setDisplay("heatEco", ""); + setDisplay("heatDefault", "none"); + } else { + setDisplay("heatEco", "none"); + setDisplay("heatDefault", ""); + } + if (evMode == "Eco") { + setDisplay("evEco", ""); + setDisplay("evNextTrip", "none"); + setDisplay("evDefault", "none"); + } else if (evMode == "Next Trip") { + setDisplay("evEco", "none"); + setDisplay("evNextTrip", ""); + setDisplay("evDefault", "none"); + } else { + setDisplay("evEco", "none"); + setDisplay("evNextTrip", "none"); + setDisplay("evDefault", ""); + } + if (heatOG) { + setDisplay("heatOG", ""); + } else { + setDisplay("heatOG", "none"); + } + if (heatEG) { + setDisplay("heatEG", ""); + } else { + setDisplay("heatEG", "none"); + } + if (plugev != "no car") { + setDisplay("evPlug", ""); + } else { + setDisplay("evPlug", "none"); + } + if (evModeOG == "Eco") { + setDisplay("evEcoOG", ""); + setDisplay("evNextTripOG", "none"); + setDisplay("evDefaultOG", "none"); + } else if (evModeOG == "Next Trip") { + setDisplay("evEcoOG", "none"); + setDisplay("evNextTripOG", ""); + setDisplay("evDefaultOG", "none"); + } else { + setDisplay("evEcoOG", "none"); + setDisplay("evNextTripOG", "none"); + setDisplay("evDefaultOG", ""); + } + if (evPlugOG > 0) { + setDisplay("evPlugOG", ""); + } else { + setDisplay("evPlugOG", "none"); + } + if (grid < 0) { + setAttr("gridArc", "d", describeArc(100, 100, 95, 0, -grid / angleFactor)); + setAttr("consGridani", "class", "stream-rev"); + setFlowWidth("consGridani", 55 * (1 - Math.exp(grid / 2000))); + } else { + setAttr("gridArc", "d", describeArc(100, 100, 95, 0, grid / angleFactor)); + setAttr("consGridani", "class", "stream"); + setFlowWidth("consGridani", 55 * (1 - Math.exp(-grid / 2000))); + } + + setFlowWidth("consEVani", 55 * (1 - Math.exp(-evPower))); + setFlowWidth("consEVOGani", 55 * (1 - Math.exp(-evPowerOG))); + setFlowWidth("consOGani", 55 * (1 - Math.exp(og / 2000))); + + setFlowWidth("consUGani", 55 * (1 - Math.exp( -ug / 2000))); + + setFlowWidth("consHeatAni", 55 * (1 - Math.exp(-Pheat / 2000))); + setFlowWidth("consAni", 55 * (1 - Math.exp(cons / 2000))); + setFlowWidth("consEGani", 55 * (1 - Math.exp(-eg / 2000))); + setFlowWidth("pvani", 55 * (1 - Math.exp(-pv / 2000))); +} + +} + +var chartSettings = { + type: 'line', + options: { + animation: true, + plugins: { + annotation: { + common: { type: 'box', drawTime: 'beforeDatasetsDraw', yScaleID: 'y-axis-0', backgroundColor: 'rgba(255, 255, 255, 0.05)', init: true }, + annotations: [] + }, + tooltip: { + position: 'nearest', + pointStyle: "circle", + boxWidth: 4, + usePointStyle: true, + callbacks: { + footer: function (tooltipItems){return ""}, + }, + }, + legend: { + position: "bottom", + labels: { + pointStyleWidth: 10, + usePointStyle: true, + pointStyle: "line", + } + }, + }, + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + mode: 'index', + }, + scales: { + x: { + adapters: { + date: { + locale: "DE-de" + } + }, + ticks: { + + }, + type: 'timestack', + }, + y: { + stacked: true, + display: true, + min: 0, + position: 'left', + ticks: { + callback: value => `${value / 1000} kW`, + }, + /*title: { + display: true, + text: "Leistung" + }*/ + }, + y1: { + stacked: false, + display: true, + position: 'right', + min: 0, + max: 100, + ticks: { + callback: value => `${value} %`, + }, + /*title: { + display: true, + text: "Ladestand" + },*/ + data:{} + } + } + } + }; + + var forecastChartSettings = { + type: 'bar', + options: { + animation: true, + plugins: { + tooltip: { + position: 'nearest', + pointStyle: "circle", + boxWidth: 4, + usePointStyle: true, + callbacks: { + footer: function (tooltipItems){return ""}, + }, + }, + legend: { + position: "bottom", + labels: { + pointStyleWidth: 10, + usePointStyle: true, + pointStyle: "line", + } + }, + }, + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + mode: 'index', + }, + scales: { + x: { + adapters: { + date: { + locale: "DE-de" + } + }, + ticks: { + }, + type: 'timestack', + timestack:{ + right_floating_tick_thres: 0.3, + format_style: {month: 'long'}, + } + }, + y: { + stacked: false, + display: true, + position: 'left', + ticks: { + callback: value => `${value / 1000} kW`, + }, + /*title: { + display: true, + text: "Leistung" + }*/ + }, + } + } + }; + + var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), { + keyboard: false + }); + var chartData = {}; + var mqttData = {}; + var timeFrom = -24; + var timeTo = 12; + const dayRange = () => ({ FROM: timeFrom, TO: timeTo }); + const consChart = new Chart( + document.querySelector('#consumption-chart'), + Object.assign({}, chartSettings) + ); + const prodChart = new Chart( + document.querySelector('#production-chart'), + Object.assign({}, chartSettings) + ); + + const foreChart = new Chart( + document.querySelector('#forecast-chart'), + Object.assign({}, forecastChartSettings) + ); + + function updateCharts(){ + getData(consChart, 'ajax/getConsData.php', { params: dayRange, sunrise: true, autoUpdate: false, updateMode: "none" }); + getData(prodChart, 'ajax/getProdData.php', { params: dayRange, sunrise: true, autoUpdate: false, updateMode: "none" }); + } + + function prevDay(){ + timeFrom -= 24; + timeTo -= 24; + + var now = new Date(); + now.setDate(now.getDate()+1+(timeFrom/24)); + var day = ("0" + now.getDate()).slice(-2); + var month = ("0" + (now.getMonth() + 1)).slice(-2); + var selected = now.getFullYear()+"-"+(month)+"-"+(day); + document.getElementById("DatePickerCons").value = selected; + document.getElementById("DatePickerProd").value = selected; + updateCharts(); + } + + function changeDay(e){ + const date1 = new Date(e.target.value); + const date2 = new Date(); + const difference = Math.floor((date1.getTime() - date2.getTime()) / (1000*60*60*24)) *24; + timeFrom = difference; + timeTo = difference+36; + updateCharts(); + //.then((ret) => enableEvents()); + } + + function nextDay(){ + timeFrom += 24; + timeTo += 24; + var now = new Date(); + now.setDate(now.getDate()+1+(timeFrom/24)); + var day = ("0" + now.getDate()).slice(-2); + var month = ("0" + (now.getMonth() + 1)).slice(-2); + var selected = now.getFullYear()+"-"+(month)+"-"+(day); + document.getElementById("DatePickerCons").value = selected; + document.getElementById("DatePickerProd").value = selected; + updateCharts(); + } + + document.addEventListener('readystatechange', function () { + if (event.target.readyState === "complete") { + solarMQTT.getMQTT(); + getData(consChart, 'ajax/getConsData.php', { params: dayRange, sunrise: true, updateMode: "none" }); + getData(prodChart, 'ajax/getProdData.php', { params: dayRange, sunrise: true, updateMode: "none" }); + getData(foreChart, 'ajax/getForecastData.php', { params: dayRange, updateMode: "none" }); + getStats("Stats-Year","ajax/getStats.php?type=ThisYear"); + solarSVG.fillElementArray(); + mountMeteogram(); + } + }); + + + + // Setzt den Schieberegler aus den Wattstunden, die die Wallbox meldet. + function setzeLadeRegler(wattstunden) { + const regler = document.getElementById("modal-slider"); + const whProProzent = Number(regler.dataset.whProProzent); + if (whProProzent > 0) { + regler.value = Math.round(Number(String(wattstunden)) / whProProzent); + } + } + + function openModal(type) { + document.getElementById("modalSaveBtn").style.display = type === "watering" ? "none" : ""; + switch(type){ + case "CarEG": + document.getElementById("modal-title").innerHTML = "Autoladung EG"; + contentURL = "ajax/carEG.php"; + break; + case "CarOG": + document.getElementById("modal-title").innerHTML = "Autoladung OG"; + contentURL = "ajax/carOG.php"; + break; + case "heater": + document.getElementById("modal-title").innerHTML = "Steuerung Heizstab"; + contentURL = "ajax/heater.php"; + break; + case "watering": + document.getElementById("modal-title").innerHTML = "Gartenbewässerung"; + contentURL = window.location.pathname.replace(/\/index\.php$/, "").replace(/\/$/, "") + "/ajax/watering.php"; + break; + default: + document.getElementById("modal-title").innerHTML = "Fehler"; + contentURL = ""; + break; + } + + modalBodyElement = document.getElementById('modal-body'); + modalBodyElement.innerHTML = loadingHTML("Wird geladen..."); + document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax); + document.getElementById("modalSaveBtn").contentURL = contentURL; + modalEV.show(); + fetch(contentURL, { + method: 'GET', + headers: { + 'X-Requested-From-Modal': 'a', + 'Requested-With-Ajax': 'ajax' + } + }) + .then(response => response.text()) + .then(html => { + modalBodyElement.innerHTML = html; + + var slider = document.getElementById("modal-slider"); + var span = document.getElementById("modal-slider-label"); + switch(type){ + case "CarEG": + bindModalSlider(slider, span, value => value + "%"); + document.getElementById("evStart/Stop").addEventListener("click", submitFormAjax); + document.getElementById("evStart/Stop").contentURL = contentURL; + mode = new TextDecoder().decode(mqttData["wattpilot/properties/lmo/state"]); + time = String(mqttData["wattpilot/properties/ftt/state"]); + energy = mqttData["wattpilot/properties/fte/state"]; + amp = new TextDecoder().decode(mqttData["wattpilot/properties/amp/state"]); + car = new TextDecoder().decode(mqttData["wattpilot/properties/car/state"]); + charge = Number(mqttData["solarManager/p_l1ev"])+Number(mqttData["solarManager/p_l2ev"])+Number(mqttData["solarManager/p_l3ev"]) + if (charge < 0.2) { document.getElementById("evStart/Stop").innerHTML = 'Laden starten' } + else { document.getElementById("evStart/Stop").innerHTML = 'Laden stoppen' } + if (mode == "Awattar") { document.getElementById("EV_Eco").click(); } + else if (mode == "Default") { document.getElementById("EV_Default").click(); } + else if (mode == "AutomaticStop") { document.getElementById("EV_NextTrip").click(); } + else { alert(mode); } + document.getElementById("EV_NextTripTime").value = time.toHHMM(); + // Wattstunden je Prozent liefert das Formular mit (carForm.php); + // die Akkugroesse steht in restricted/config.php. Frueher standen + // hier feste 14000 Wh - fuer beide Autos dasselbe, und fuer keines + // von beiden richtig. + setzeLadeRegler(energy); + document.getElementById("modal-slider").dispatchEvent(new Event('input')); + if (amp == "16") { document.getElementById("EV_16A").click(); } + else if (amp == "10") { document.getElementById("EV_10A").click(); } + else if (amp == "6") { document.getElementById("EV_6A").click(); } + break; + case "CarOG": + bindModalSlider(slider, span, value => value + "%"); + document.getElementById("evStart/Stop").addEventListener("click", submitFormAjax); + document.getElementById("evStart/Stop").contentURL = contentURL; + mode = mqttData["go-eCharger/270003/lmo"]; + time = String(mqttData["go-eCharger/270003/att"]); + energy = mqttData["go-eCharger/270003/ate"]; + amp = mqttData["go-eCharger/270003/amp"]; + car = mqttData["go-eCharger/270003/car"]; + if (car != "2") { document.getElementById("evStart/Stop").innerHTML = 'Laden starten' } + else { document.getElementById("evStart/Stop").innerHTML = 'Laden stoppen' } + if (mode == "4") { document.getElementById("EV_Eco").click(); } + else if (mode == "3") { document.getElementById("EV_Default").click(); } + else if (mode == "5") { document.getElementById("EV_NextTrip").click(); } + else { alert(mode); } + document.getElementById("EV_NextTripTime").value = time.toHHMM(); + // Wattstunden je Prozent liefert das Formular mit (carForm.php); + // die Akkugroesse steht in restricted/config.php. Frueher standen + // hier feste 14000 Wh - fuer beide Autos dasselbe, und fuer keines + // von beiden richtig. + setzeLadeRegler(energy); + document.getElementById("modal-slider").dispatchEvent(new Event('input')); + if (amp == "16") { document.getElementById("EV_16A").click(); } + else if (amp == "10") { document.getElementById("EV_10A").click(); } + else if (amp == "6") { document.getElementById("EV_6A").click(); } + break; + case "heater": + bindModalSlider(slider, span, value => value == 0 ? "Automatik" : (value / 10) + " kW"); + if(mqttData["solarManager/heatMode"] == "eco"){ + document.getElementById("modal-slider").value = 0; + }else{ + document.getElementById("modal-slider").value = Math.round(mqttData["solarManager/pHeat"]/100); + } + document.getElementById("modal-slider").dispatchEvent(new Event('input')); + break; + case "watering": + initializeWateringModal(); + break; + } + + }) + .catch(error => { + modalBodyElement.innerHTML = error.message; + }); + } + + const wateringTimersLastSeen = {}; + // Eine Tabelle fuer alles: Ventil, Automatik-Zone des Skripts, Beschriftung + // und die Anzeige-Ids im Realtime-SVG. Vorher standen dieselben Zuordnungen + // in drei getrennten Tabellen. + const wateringZones = { + troughs: { + topic: "Gartenwasser/vorn", valve: 1, auto: "troege", label: "Tröge", + statusId: "wateringTroughStatus", iconId: "wateringTroughIcon" + }, + frontGarden: { + topic: "Gartenwasser/vorn", valve: 6, auto: "garten_vorn", label: "Vorn", + statusId: "wateringFrontStatus", iconId: "wateringFrontIcon" + }, + raisedBeds: { + topic: "Gartenwasser/hinten", valve: 6, auto: "hochbeete", label: "Beete", + statusId: "wateringBedsStatus", iconId: "wateringBedsIcon" + } + }; + // Feste Liste - der Sekundentakt soll sie nicht jedes Mal neu aufbauen. + const wateringZoneList = Object.entries(wateringZones); + + function wateringCommand(topic, command, directTopic = false) { + const feedback = document.getElementById("watering-feedback"); + const targetTopic = directTopic ? topic : topic + "/set"; + const payload = typeof command === "string" ? command : JSON.stringify(command); + const endpoint = window.location.pathname.replace(/\/index\.php$/, "").replace(/\/$/, "") + "/ajax/watering.php"; + const showFeedback = (message, type) => { + if (!feedback) return; + feedback.textContent = message; + feedback.className = "alert alert-" + type + " mb-3"; + feedback.style.whiteSpace = "pre-line"; + }; + fetch(endpoint, { + method: "POST", + credentials: "same-origin", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ topic: targetTopic, payload: payload }) + }).then(async (response) => { + const result = await response.json(); + if (!response.ok || !result.success) throw new Error(result.message || "Unbekannter MQTT-Fehler."); + showFeedback("Befehl gesendet.\nTopic: " + result.topic + "\nInhalt: " + result.payload, "success"); + }).catch((error) => { + showFeedback("Befehl konnte nicht gesendet werden: " + error.message + "\nTopic: " + targetTopic + "\nInhalt: " + payload, "danger"); + }); + } + + function initializeWateringModal() { + document.querySelectorAll("[data-watering-toggle]").forEach((button) => { + const updateButton = () => { + const zone = wateringZones[button.dataset.wateringZone]; + setzeZonenButton(button, wateringLabel(zone.topic, zone.valve)); + }; + button.addEventListener("click", () => { + const zone = wateringZones[button.dataset.wateringZone]; + if (wateringLabel(zone.topic, zone.valve) === "aus") { + const timerInput = document.querySelector(`[data-watering-timer='${button.dataset.wateringZone}']`); + const timer = Math.min(120, Math.max(1, Number(timerInput.value) || 30)); + wateringCommand(zone.topic, { valve: zone.valve, set: "open", timer: timer }); + } else { + wateringCommand(zone.topic, { valve: zone.valve, set: "close" }); + } + }); + updateButton(); + }); + document.querySelectorAll("[data-watering-auto-toggle]").forEach((button) => { + button.addEventListener("click", () => { + const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(","); + const modes = button.dataset.wateringAuto.split(","); + const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index])); + topics.forEach((topic, index) => { + wateringCommand(topic + "/auto", active ? "Stop" : modes[index], true); + }); + }); + updateWateringAutoButton(button); + }); + updateWateringStatus(); + } + + // Die Anzeige laeuft im Sekundentakt, die Nutzlasten aendern sich aber nur, + // wenn eine neue MQTT-Nachricht kommt. messageReceived legt dabei jedes Mal + // ein neues Buffer-Objekt ab - der Identitaetsvergleich reicht also als + // Gueltigkeitspruefung, ein Invalidieren von Hand braucht es nicht. + const wateringJsonCache = {}; + const wateringDecoder = new TextDecoder(); + + function wateringJson(topic) { + const value = mqttData[topic]; + if (!value) return null; + const cached = wateringJsonCache[topic]; + if (cached && cached.raw === value) return cached.parsed; + + let parsed = null; + try { + parsed = JSON.parse(typeof value === "string" ? value : wateringDecoder.decode(value)); + } catch (error) { + parsed = null; + } + wateringJsonCache[topic] = { raw: value, parsed: parsed }; + return parsed; + } + + function wateringHistory(topic, valve) { + const data = wateringJson(topic + "/LastWatering"); + if (!data) return { dateTime: "--", duration: "--" }; + return { + dateTime: data["wateringDateTime" + valve] || "--", + duration: data["wateringDurationSecs" + valve] !== undefined + ? wateringTimeLabel(Number(data["wateringDurationSecs" + valve])) + : "--" + }; + } + + // Tagesdatum im Format der Ventilsteuerung ("29.08.26"). + function wateringDayStamp(date) { + const zwei = (zahl) => String(zahl).padStart(2, "0"); + return zwei(date.getDate()) + "." + zwei(date.getMonth() + 1) + "." + zwei(date.getFullYear() % 100); + } + + // Tagessummen der Ventilsteuerung: WateringToday fuer den laufenden Tag, + // LastWateringDay fuer den letzten Tag, an dem ueberhaupt bewaessert wurde. + // Beide Nutzlasten tragen den zugehoerigen Tag als "wateringDay" mit. Bei + // WateringToday wird der geprueft - die Nachricht ist retained, nach einem + // Tageswechsel ohne Bewaesserung stuende sonst die Summe von gestern als + // "heute" in der Karte. + function wateringDayTotal(topic, subTopic, feld, valve, mussHeuteSein) { + const data = wateringJson(topic + "/" + subTopic); + if (!data) return { label: "--", day: "" }; + const tag = data.wateringDay || ""; + const sekunden = data[feld + valve]; + if (sekunden === undefined) return { label: "--", day: tag }; + if (mussHeuteSein && tag !== wateringDayStamp(new Date())) return { label: "--", day: tag }; + return { label: wateringTimeLabel(Number(sekunden)), day: tag }; + } + + function wateringAutoActive(topic, mode) { + const timers = wateringJson(topic + "/Timers"); + return !!(timers && timers.auto === mode); + } + + function wateringValue(data, valve) { + if (!data) return null; + const candidates = [ + data.timers && data.timers[valve], + data.timers && data.timers[String(valve)], + data[valve], + data[String(valve)], + data.valves && data.valves[valve], + data.valves && data.valves[String(valve)], + data.status && data.status[valve], + data.status && data.status[String(valve)], + data.valveState && data.valveState[valve], + data.valveState && data.valveState[String(valve)], + data.setValveState && data.setValveState[valve], + data.setValveState && data.setValveState[String(valve)] + ]; + for (const candidate of candidates) { + if (candidate !== undefined) return candidate; + } + return null; + } + + function wateringState(value) { + if (value && typeof value === "object") { + return value.state ?? value.status ?? value.valveState ?? value.set; + } + return value; + } + + function wateringSeconds(value) { + if (value && typeof value === "object") { + value = value.remaining ?? value.timer ?? value.seconds ?? value.time; + } + return Number(value); + } + + function wateringLabel(topic, valve) { + const timers = wateringJson(topic + "/Timers"); + const status = wateringJson(topic + "/status"); + const lastSeen = wateringTimersLastSeen[topic + "/Timers"]; + if (!lastSeen || Date.now() - lastSeen > 3000) return "aus"; + const remaining = wateringValue(timers, valve); + if (remaining !== null) { + const seconds = wateringSeconds(remaining); + if (seconds !== 0) return wateringTimeLabel(Math.abs(seconds)); + return "aus"; + } + const state = wateringState(wateringValue(status, valve)); + if (state === true || state === 1 || state === "1" || state === "open" || state === "OPEN") return "an"; + if (state === false || state === 0 || state === "0" || state === "close" || state === "closed" || state === "CLOSED") return "aus"; + return "--"; + } + + function wateringTimeLabel(seconds) { + if (seconds < 60) return seconds + "s"; + return Math.ceil(seconds / 60) + "m"; + } + + function wateringIsPaused(topic, valve) { + const timers = wateringJson(topic + "/Timers"); + const remaining = wateringValue(timers, valve); + return remaining !== null && wateringSeconds(remaining) < 0; + } + + // Liest den von auto_watering.py stündlich veröffentlichten Regen-Status + // (retained, siehe Gartenwasser/AutoWatering//RainStatus) und sagt, + // ob für die Zone heute noch eine automatische Bewässerung aussteht. + function wateringAutoPlanned(autoZoneKey) { + const status = wateringJson("Gartenwasser/AutoWatering/" + autoZoneKey + "/RainStatus"); + return !!(status && status.would_water && !status.watered_recently); + } + + // Zeitraeume und Regenmengen in Klartext. "2 T." war zu kryptisch. + function wateringTage(tage) { + const zahl = Number(tage); + const text = Number.isInteger(zahl) ? String(zahl) : String(zahl).replace(".", ","); + return zahl === 1 ? text + " Tag" : text + " Tagen"; + } + + function wateringMm(wert) { + return String(wert).replace(".", ",") + " mm"; + } + + // Sagt in einem Satz, was die Automatik fuer die Zone vorhat und warum. + // Speist sich aus derselben RainStatus-Nachricht wie die Anzeige im SVG. + // Der Titel nennt zusaetzlich die Regel, nach der entschieden wird - die + // Schwellenwerte unterscheiden sich je Zone deutlich. + function wateringPlanText(autoZoneKey) { + const status = wateringJson("Gartenwasser/AutoWatering/" + autoZoneKey + "/RainStatus"); + if (!status) return { text: "keine Planungsdaten", titel: "" }; + + const regel = []; + if (status.threshold_mm !== undefined && status.threshold_days !== undefined) { + regel.push("Bewässert wird, wenn in " + wateringTage(status.threshold_days) + + " weniger als " + wateringMm(status.threshold_mm) + " Regen fallen."); + } + if (status.max_dry_days) { + regel.push("Ebenso, wenn seit " + wateringTage(status.max_dry_days) + " nicht bewässert wurde."); + } + const titel = regel.join(" "); + + if (status.watered_recently) { + const zeitpunkt = status.last_watering + ? " – zuletzt " + String(status.last_watering).replace(/[.]\d{4} /, ". ") + : ""; + return { text: "erledigt" + zeitpunkt, titel: titel }; + } + const regen = status.rain_mm !== undefined && status.threshold_days !== undefined + ? wateringMm(status.rain_mm) + " Regen in " + wateringTage(status.threshold_days) + : ""; + if (!status.would_water) { + return { text: "kein Bedarf" + (regen ? " – " + regen : ""), titel: titel }; + } + if (status.forced_dry) { + const trocken = status.max_dry_days + ? " – seit " + wateringTage(status.max_dry_days) + " nicht bewässert" + : " – lange nicht bewässert"; + return { text: "heute geplant" + trocken, titel: titel }; + } + return { text: "heute geplant" + (regen ? " – nur " + regen : ""), titel: titel }; + } + + // Alle Zonen stammen aus demselben Lauf des Skripts - der erste gefundene + // Zeitstempel steht daher fuer alle. Die Sekunden schneidet slice(0, 16) ab. + function wateringPlanUpdated() { + for (const [, zone] of wateringZoneList) { + const status = wateringJson("Gartenwasser/AutoWatering/" + zone.auto + "/RainStatus"); + if (status && status.updated) return "Planung vom " + String(status.updated).slice(0, 16); + } + return ""; + } + + function updateWateringStatus() { + wateringZoneList.forEach(([key, zone]) => { + const state = wateringLabel(zone.topic, zone.valve); + const paused = wateringIsPaused(zone.topic, zone.valve); + const planned = state === "aus" && wateringAutoPlanned(zone.auto); + + // Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement. + const status = svgElement(zone.statusId); + const icon = svgElement(zone.iconId); + if (status && icon) updateWateringIndicator(status, icon, zone.label, state, paused, planned); + + // Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen. + setzeZonenButton(document.querySelector(`[data-watering-toggle][data-watering-zone='${key}']`), state); + setzeSchild(`[data-watering-state='${key}']`, state, paused, planned); + + const history = wateringHistory(zone.topic, zone.valve); + setzeZelle(`[data-watering-last='${key}']`, history.dateTime); + setzeZelle(`[data-watering-duration='${key}']`, history.duration); + const heute = wateringDayTotal(zone.topic, "WateringToday", "wateringDurationTodaySecs", zone.valve, true); + const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false); + setzeZelle(`[data-watering-today='${key}']`, heute.label, heute.day && "Stand: " + heute.day); + setzeZelle(`[data-watering-prevday='${key}']`, vortag.label, vortag.day && "Bewässerungstag: " + vortag.day); + const plan = wateringPlanText(zone.auto); + setzeZelle(`[data-watering-plan='${key}']`, plan.text, plan.titel); + }); + document.querySelectorAll("[data-watering-auto-toggle]").forEach(updateWateringAutoButton); + setzeZelle("[data-watering-plan-updated]", wateringPlanUpdated()); + } + + // Der Knopf zeigt die naechste Aktion, nicht den Zustand: laeuft die Zone, + // steht "Aus" darauf. + function setzeZonenButton(button, state) { + if (!button) return; + const beschriftung = state === "aus" ? "Ein" : "Aus"; + if (button.textContent !== beschriftung) button.textContent = beschriftung; + button.classList.toggle("btn-success", state === "aus"); + button.classList.toggle("btn-danger", state !== "aus"); + } + + // Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen, + // pausiert gelb, von der Automatik geplant blau, sonst grau. + function setzeSchild(selektor, state, paused, planned) { + const schild = document.querySelector(selektor); + if (!schild) return; + let text = "unbekannt"; + let farbe = "bg-secondary"; + if (state !== "--" && state !== "aus") { + text = paused ? "pausiert " + state : "läuft " + state; + farbe = paused ? "bg-warning text-dark" : "bg-success"; + } else if (state === "aus") { + text = planned ? "heute geplant" : "aus"; + farbe = planned ? "bg-primary" : "bg-secondary"; + } + if (schild.textContent !== text) schild.textContent = text; + const klasse = "badge " + farbe; + if (schild.className !== klasse) schild.className = klasse; + } + + // Schreibt nur bei echter Aenderung - die Tabelle wird im Sekundentakt + // aktualisiert, ihre Werte aendern sich aber hoechstens minuetlich. + function setzeZelle(selektor, text, titel) { + const element = document.querySelector(selektor); + if (!element) return; + if (element.textContent !== text) element.textContent = text; + if (titel) { + if (element.getAttribute("title") !== titel) element.setAttribute("title", titel); + } else if (element.hasAttribute("title")) { + element.removeAttribute("title"); + } + } + + function updateWateringAutoButton(button) { + const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(","); + const modes = button.dataset.wateringAuto.split(","); + const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index])); + if (!button.dataset.wateringLabel) { + button.dataset.wateringLabel = button.textContent.replace(/^Automatik\s+/, ""); + } + button.textContent = (active ? "Aus: " : "Ein: ") + button.dataset.wateringLabel; + button.classList.toggle("btn-outline-primary", !active); + button.classList.toggle("btn-outline-danger", active); + } + + function updateWateringIndicator(status, icon, name, state, paused, planned) { + // "Aus" bleibt bei nicht geplant stehen; bei geplant ersetzt die + // Stoppuhr (U+23F1, ohne Emoji-Variationsselektor -> einfarbig, folgt + // der Icon-Farbe) den Text, um Platz auf der textPath zu sparen. + // Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung + // wuerde jede Sekunde derselbe Wert geschrieben und das Icon samt + // Filter neu gezeichnet. + const text = state === "aus" ? (planned ? "⏱" : "Aus") : state; + if (status.textContent !== text) status.textContent = text; + // Geplant-Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water"). + const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : planned ? "#3A6292" : "#6c757d"; + if (icon.getAttribute("color") !== color) { + icon.setAttribute("color", color); + icon.setAttribute("fill", color); + icon.style.color = color; + } + const titel = name + ": " + (state === "aus" && planned ? "Aus (Automatik heute noch geplant)" : state); + const titelKnoten = icon.querySelector("title"); + if (titelKnoten.textContent !== titel) titelKnoten.textContent = titel; + } + + window.setInterval(updateWateringStatus, 1000); + + + + + + + function polarToCartesian(centerX, centerY, radius, angleInDegrees) { + var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; + + return { + x: centerX + (radius * Math.cos(angleInRadians)), + y: centerY + (radius * Math.sin(angleInRadians)) + }; + } + + function getRBGComponent(colRange, minCol, valRange, minVal, val) { + return Math.round(((val - minVal) / valRange) * colRange + minCol) + .toString(16) + .toUpperCase() + .padStart(2, '0'); + } + + function assignColor(minCol, maxCol, minVal, maxVal, val) { + var color = ""; + var minR = parseInt(minCol.substring(1, 3), 16); + var maxR = parseInt(maxCol.substring(1, 3), 16); + var minG = parseInt(minCol.substring(3, 5), 16); + var maxG = parseInt(maxCol.substring(3, 5), 16); + var minB = parseInt(minCol.substring(5, 7), 16); + var maxB = parseInt(maxCol.substring(5, 7), 16); + var valsRange = maxVal - minVal; + var rangeG = maxG - minG; + var rangeR = maxR - minR; + var rangeB = maxB - minB; + if (val > maxVal) + val = maxVal; + else if (val < minVal) + val = minVal; + + color = '#' + + getRBGComponent(rangeR, minR, valsRange, minVal, val) + + getRBGComponent(rangeG, minG, valsRange, minVal, val) + + getRBGComponent(rangeB, minB, valsRange, minVal, val); + return color; + } + + function describeArc(x, y, radius, startAngle, endAngle) { + if (endAngle > 288) { + endAngle = 288; + } + // Auf volle Grad runden: bei Radius 95 ist ein Grad rund 1,7 Einheiten, + // feiner sieht niemand. Dafuer bleibt das d-Attribut bei kleinen + // Schwankungen unveraendert und setAttr unterdrueckt den Schreibvorgang - + // sonst laesst jeder Zappler den Bogen samt Filter neu zeichnen. + startAngle = Math.round(startAngle); + endAngle = Math.round(endAngle); + var start = polarToCartesian(x, y, radius, endAngle); + var end = polarToCartesian(x, y, radius, startAngle); + var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; + // zwei Nachkommastellen genuegen und halten das Attribut kurz + var rnd = (v) => Math.round(v * 100) / 100; + return [ + "M", rnd(start.x), rnd(start.y), + "A", radius, radius, 0, largeArcFlag, 0, rnd(end.x), rnd(end.y) + ].join(" "); + } diff --git a/restricted/deviceDiscovery/device_discovery.py b/restricted/deviceDiscovery/device_discovery.py index ef11b6b..38f0ed9 100644 --- a/restricted/deviceDiscovery/device_discovery.py +++ b/restricted/deviceDiscovery/device_discovery.py @@ -1,644 +1,644 @@ -#!/usr/bin/env python3 -""" -Device Discovery - Refactored with Unified Architecture -======================================================= -Zentrale Datenbank-Logik im Hauptscript -Einheitliche Schnittstelle für alle Module -""" - -import pymysql -from pymysql import Error -import json -import logging -import configparser -import os -from typing import Optional, List, Dict - -# Import Module -from modules.tahoma_module import TahomaModule -from modules.wled_module import WLEDModule -from modules.mqtt_module import MQTTModule -from modules.shelly_module import ShellyModule -from modules.gartenwasser_module import GartenwasserModule -from modules.logic_module import LogicModule #nicht Vergessen neue Module auch in Zeile 546 einzufügen!! - -# Logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - - -# ============================================================================ -# CONFIG CLASS (Original) -# ============================================================================ - -class Config: - """Lädt und verwaltet die Konfiguration""" - - def __init__(self, config_file: str = 'config.ini'): - script_dir = os.path.dirname(os.path.abspath(__file__)) - config_path = os.path.join(script_dir, config_file) - - if not os.path.exists(config_path): - raise FileNotFoundError( - f"Konfigurationsdatei '{config_file}' nicht gefunden in {script_dir}" - ) - - self.config = configparser.ConfigParser() - self.config.read(config_path, encoding='utf-8') - logger.info(f"Konfiguration geladen von: {config_path}") - self._validate() - - def _validate(self): - required_sections = ['database', 'options'] - for section in required_sections: - if not self.config.has_section(section): - raise ValueError(f"Erforderliche Sektion '[{section}]' fehlt") - - def _get_bool(self, section: str, key: str, fallback: bool = False) -> bool: - value = self.config.get(section, key, fallback=str(fallback)).strip().lower() - return value in ('true', '1', 'yes', 'on') - - def _get_int(self, section: str, key: str, fallback: int = 0) -> int: - try: - return self.config.getint(section, key, fallback=fallback) - except ValueError: - return fallback - - def _get_list(self, section: str, key: str) -> List[str]: - value = self.config.get(section, key, fallback='').strip() - if not value: - return [] - return [item.strip() for item in value.split(',') if item.strip()] - - # Database - @property - def db_host(self) -> str: - return self.config.get('database', 'host').strip() - - @property - def db_port(self) -> int: - return self._get_int('database', 'port', 3306) - - @property - def db_name(self) -> str: - return self.config.get('database', 'database').strip() - - @property - def db_user(self) -> str: - return self.config.get('database', 'user').strip() - - @property - def db_password(self) -> str: - return self.config.get('database', 'password').strip() - - # Tahoma - @property - def tahoma_enable(self) -> bool: - if not self.config.has_section('tahoma'): - return False - return self._get_bool('tahoma', 'enable', False) - - @property - def tahoma_ip(self) -> str: - if not self.config.has_section('tahoma'): - return '' - return self.config.get('tahoma', 'ip', fallback='').strip() - - @property - def tahoma_token(self) -> str: - if not self.config.has_section('tahoma'): - return '' - return self.config.get('tahoma', 'token', fallback='').strip() - - @property - def tahoma_timeout(self) -> int: - if not self.config.has_section('tahoma'): - return 10 - return self._get_int('tahoma', 'timeout', 10) - - # WLED - @property - def wled_enable(self) -> bool: - if not self.config.has_section('wled'): - return False - return self._get_bool('wled', 'enable', False) - - @property - def wled_discovery_timeout(self) -> int: - if not self.config.has_section('wled'): - return 5 - return self._get_int('wled', 'discovery_timeout', 5) - - @property - def wled_manual_ips(self) -> List[str]: - if not self.config.has_section('wled'): - return [] - return self._get_list('wled', 'manual_ips') - - @property - def wled_timeout(self) -> int: - if not self.config.has_section('wled'): - return 2 - return self._get_int('wled', 'timeout', 2) - - # LOGIC - @property - def logic_enable(self) -> bool: - if not self.config.has_section('logic'): - return False - return self._get_bool('logic', 'enable', False) - - # GARTENWASSER - @property - def gartenwasser_enable(self) -> bool: - if not self.config.has_section('gartenwasser'): - return False - return self._get_bool('gartenwasser', 'enable', False) - - - # MQTT - @property - def mqtt_enable(self) -> bool: - if not self.config.has_section('mqtt'): - return False - return self._get_bool('mqtt', 'enable', False) - - @property - def mqtt_broker(self) -> str: - if not self.config.has_section('mqtt'): - return 'localhost' - return self.config.get('mqtt', 'broker', fallback='localhost').strip() - - @property - def mqtt_port(self) -> int: - if not self.config.has_section('mqtt'): - return 1883 - return self._get_int('mqtt', 'port', 1883) - - @property - def mqtt_username(self) -> Optional[str]: - if not self.config.has_section('mqtt'): - return None - value = self.config.get('mqtt', 'username', fallback='').strip() - return value if value else None - - @property - def mqtt_password(self) -> Optional[str]: - if not self.config.has_section('mqtt'): - return None - value = self.config.get('mqtt', 'password', fallback='').strip() - return value if value else None - - @property - def mqtt_discovery_prefix(self) -> str: - if not self.config.has_section('mqtt'): - return 'homeassistant' - return self.config.get('mqtt', 'discovery_prefix', fallback='homeassistant').strip() - - @property - def mqtt_discovery_timeout(self) -> int: - if not self.config.has_section('mqtt'): - return 10 - return self._get_int('mqtt', 'discovery_timeout', 10) - - # Shelly - @property - def shelly_enable(self) -> bool: - if not self.config.has_section('shelly'): - return False - return self._get_bool('shelly', 'enable', False) - - @property - def shelly_network_range(self) -> str: - if not self.config.has_section('shelly'): - return '192.168.1' - return self.config.get('shelly', 'network_range', fallback='192.168.1').strip() - - @property - def shelly_start_ip(self) -> int: - if not self.config.has_section('shelly'): - return 1 - return self._get_int('shelly', 'start_ip', 1) - - @property - def shelly_end_ip(self) -> int: - if not self.config.has_section('shelly'): - return 254 - return self._get_int('shelly', 'end_ip', 254) - - # Options - @property - def clear_tables(self) -> bool: - return self._get_bool('options', 'clear_tables', True) - - @property - def log_level(self) -> str: - level = self.config.get('options', 'log_level', fallback='INFO').strip().upper() - valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] - return level if level in valid_levels else 'INFO' - - @property - def log_file(self) -> Optional[str]: - value = self.config.get('options', 'log_file', fallback='').strip() - return value if value else None - - -# ============================================================================ -# DATABASE MANAGER (Zentral im Hauptscript) -# ============================================================================ - -class DatabaseManager: - """Zentrale Datenbank-Verwaltung - ALLE DB-Operationen hier""" - - PARAMETER_TYPES = { - 0: "bool", - 1: "integer", # Integer value - 2: "float", # Floating point number - 3: "string", # Text string - 4: "undefined", - 5: "time", - 6: "array", - 7: "deltatime", - 8: "date", - 9: "datetime", - } - - PARAMETER_TYPES_MAPPING = { - "bool": 0, - "boolean" : 0, - "integer" : 1, # Integer value - "float" : 2, # Floating point number - "number" : 2, # Floating point number - "string" : 3, # Text string - "time" : 5, - "array": 6, - "deltatime" : 7, - "date" : 8, - "datetime" : 9, - } - - def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306): - self.host = host - self.database = database - self.user = user - self.password = password - self.port = port - self.connection = None - - def connect(self) -> bool: - """Stellt Verbindung zur Datenbank her""" - try: - self.connection = pymysql.connect( - host=self.host, - database=self.database, - user=self.user, - password=self.password, - port=self.port, - charset='utf8mb4' - ) - logger.info("✓ Erfolgreich mit MySQL-Datenbank verbunden") - return True - except Error as e: - logger.error(f"✗ Datenbankverbindung fehlgeschlagen: {e}") - return False - - def disconnect(self): - """Schließt Datenbankverbindung""" - if self.connection: - self.connection.close() - logger.info("Datenbankverbindung geschlossen") - - def clear_tables(self): - """Löscht alle Einträge aus allen Tabellen""" - try: - cursor = self.connection.cursor() - cursor.execute("SET FOREIGN_KEY_CHECKS=0") - cursor.execute("TRUNCATE state_types") - cursor.execute("TRUNCATE command_parameters") - cursor.execute("TRUNCATE actor_commands") - cursor.execute("TRUNCATE actor_states") - cursor.execute("TRUNCATE actors") - cursor.execute("TRUNCATE sensor_states") - cursor.execute("TRUNCATE sensors") - - cursor.execute("SET FOREIGN_KEY_CHECKS=1") - - self.connection.commit() - logger.info("✓ Alle Tabellen geleert") - cursor.close() - except Error as e: - logger.error(f"✗ Fehler beim Leeren der Tabellen: {e}") - self.connection.rollback() - - - - def insert_actor(self, device_type: str, name: str, url: str, - commands: list, states: list) -> bool: - """ - Fügt einen Aktor in die Datenbank ein - - Args: - device_type: Typ des Geräts - name: Name des Geräts - url: Eindeutige URL/ID - commands: Liste von Command-Dicts - states: Liste von State-Dicts - """ - try: - cursor = self.connection.cursor() - - # Actor einfügen - query = """ - INSERT INTO actors (type, name, url) - VALUES (%s, %s, %s) - ON DUPLICATE KEY UPDATE - type = VALUES(type), - name = VALUES(name) - """ - cursor.execute(query, (device_type, name, url)) - query = """ - SELECT ID FROM actors where url = %s - """ - cursor.execute(query, (url)) - actor_id = cursor.fetchone()[0] - - # Commands einfügen - for cmd in commands: - command_name = cmd.get('command', '') - command_url = cmd.get('url', '') - cmd_query = """ - INSERT INTO actor_commands (actor_id, command_name, command_url) - VALUES (%s, %s, %s) - ON DUPLICATE KEY UPDATE - command_url = VALUES(command_url) - """ - cursor.execute(cmd_query, (actor_id, command_name, command_url)) - query = """ - SELECT ID FROM actor_commands where actor_id = %s and command_name = %s - """ - cursor.execute(query, (actor_id, command_name)) - command_id = cursor.fetchone()[0] - - # Parameter einfügen - for param in cmd.get('parameters', []): - param_query = """ - INSERT INTO command_parameters - (command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url) - VALUES (%s, %s, %s, %s, %s, %s, %s) - ON DUPLICATE KEY UPDATE - parameter_type = VALUES(parameter_type), - min_value = VALUES(min_value), - max_value = VALUES(max_value), - possible_values = VALUES(possible_values), - url = VALUES(url) - """ - param_name = param.get('name', '') - param_type = self.PARAMETER_TYPES_MAPPING.get(param.get('type', ''), 4) - min_val = param.get('min') - max_val = param.get('max') - possible_vals = json.dumps(param.get('values')) if 'values' in param else "" - param_url = param.get('url') - - cursor.execute(param_query, - (command_id, param_name, param_type, min_val, max_val, possible_vals, param_url)) - - # States einfügen - for state in states: - state_query = """ - INSERT INTO actor_states - (actor_id, state_name, state_type, current_value, unit, url, possible_values, value_path) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s) - ON DUPLICATE KEY UPDATE - state_type = VALUES(state_type), - -- COALESCE und nicht VALUES() allein: ein Modul, das - -- keinen Startwert kennt, liefert hier NULL, und das - -- loeschte bisher den laufenden Messwert. Der Runner - -- schrieb ihn auch nicht zurueck - er merkt sich, was er - -- zuletzt in die Datenbank geschrieben hat, und der - -- naechste gleiche Wert galt damit als unveraendert. Im - -- Editor stand danach "Regen heute (= )", bis sich der - -- Wert zufaellig einmal aenderte. - current_value = COALESCE(VALUES(current_value), current_value), - possible_values = VALUES(possible_values), - unit = VALUES(unit), - url = VALUES(url), - value_path = VALUES(value_path) - """ - state_name = state.get('name', '') - state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4) - current_value = str(state.get('current_value', '')) if 'current_value' in state else None - unit = state.get('unit') - state_url = state.get('url') - value_path = state.get('value_path') - possible_vals = json.dumps(state.get('values')) if 'values' in state else "" - cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url, possible_vals, value_path)) - - self.connection.commit() - cursor.close() - return True - - except Error as e: - logger.error(f"✗ Fehler beim Einfügen des Aktors {name}: {e}") - self.connection.rollback() - return False - - def fillVariableTypes(self): - cursor = self.connection.cursor() - query = """ - INSERT INTO state_types (id, type) - VALUES (%s, %s) - ON DUPLICATE KEY UPDATE - type = VALUES(type) - """ - for id, parameter_type in self.PARAMETER_TYPES.items(): - cursor.execute(query, (id, parameter_type)) - - def insert_sensor(self, device_type: str, name: str, url: str, states: list) -> bool: - """ - Fügt einen Sensor in die Datenbank ein - - Args: - device_type: Typ des Sensors - name: Name des Sensors - url: Eindeutige URL/ID - states: Liste von State-Dicts - """ - try: - cursor = self.connection.cursor() - - # Sensor einfügen - query = """ - INSERT INTO actors (type, name, url) - VALUES (%s, %s, %s) - ON DUPLICATE KEY UPDATE - type = VALUES(type), - name = VALUES(name) - """ - cursor.execute(query, (device_type, name, url)) - sensor_id = cursor.lastrowid - query = """ - SELECT ID FROM actors where url = %s - """ - cursor.execute(query, (url)) - sensor_id = cursor.fetchone()[0] - - # States einfügen - for state in states: - state_query = """ - INSERT INTO actor_states - (actor_id, state_name, state_type, current_value, unit, url, value_path, possible_values) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s) - ON DUPLICATE KEY UPDATE - state_type = VALUES(state_type), - -- Siehe insert_actor: NULL heisst "kein Startwert", - -- nicht "Messwert loeschen". - current_value = COALESCE(VALUES(current_value), current_value), - unit = VALUES(unit), - url = VALUES(url), - value_path = VALUES(value_path), - possible_values = VALUES(possible_values) - """ - state_name = state.get('name', '') - state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4) - current_value = str(state.get('current_value', '')) if 'current_value' in state else None - unit = state.get('unit') - state_url = state.get('url') - value_path = state.get('value_path') - possible_vals = json.dumps(state.get('values')) if 'values' in state else "" - - cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url, value_path, possible_vals)) - - self.connection.commit() - cursor.close() - return True - - except Error as e: - logger.error(f"✗ Fehler beim Einfügen des Sensors {name}: {e}") - self.connection.rollback() - return False - - -# ============================================================================ -# MAIN FUNCTION -# ============================================================================ - -def main(): - """Hauptfunktion - orchestriert alle Module mit einheitlicher Schnittstelle""" - - # Konfiguration laden - try: - config = Config('config.ini') - logger.info("✓ Konfiguration erfolgreich geladen") - except FileNotFoundError as e: - print(f"FEHLER: {e}") - return - except Exception as e: - print(f"FEHLER: {e}") - return - - # Logging anpassen - log_level = getattr(logging, config.log_level) - logger.setLevel(log_level) - - if config.log_file: - file_handler = logging.FileHandler(config.log_file, encoding='utf-8') - file_handler.setLevel(log_level) - file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) - logger.addHandler(file_handler) - logger.info(f"Logging in Datei: {config.log_file}") - - # Datenbank initialisieren - logger.info("Verbinde mit MySQL-Datenbank...") - db = DatabaseManager( - config.db_host, - config.db_name, - config.db_user, - config.db_password, - config.db_port - ) - - if not db.connect(): - logger.error("Datenbankverbindung fehlgeschlagen. Abbruch.") - return - - try: - # Tabellen leeren - if config.clear_tables: - db.clear_tables() - - total_actors = 0 - total_sensors = 0 - - db.fillVariableTypes() - # Module initialisieren - modules = [ - TahomaModule(config), - WLEDModule(config), - MQTTModule(config), - ShellyModule(config), - LogicModule(config), - GartenwasserModule(config) - ] - - # Jedes Modul durchlaufen - for module in modules: - if not module.is_enabled(): - logger.info(f"Modul {module.get_name()} ist deaktiviert") - continue - - try: - # Discovery durchführen (einheitliche Schnittstelle!) - actors, sensors = module.discover() - - # Actors in DB speichern - for actor in actors: - if db.insert_actor( - actor['type'], - actor['name'], - actor['url'], - actor.get('commands', []), - actor.get('states', []) - ): - total_actors += 1 - logger.info(f" ✓ Actor: {actor['name']} ({actor['type']})") - - # Sensors in DB speichern - for sensor in sensors: - if db.insert_sensor( - sensor['type'], - sensor['name'], - sensor['url'], - sensor.get('states', []) - ): - total_sensors += 1 - logger.info(f" ✓ Sensor: {sensor['name']} ({sensor['type']})") - - except Exception as e: - logger.error(f"✗ Fehler bei Modul {module.get_name()}: {e}") - import traceback - traceback.print_exc() - - # Zusammenfassung - logger.info("\n" + "=" * 60) - logger.info("ZUSAMMENFASSUNG") - logger.info("=" * 60) - logger.info(f"Aktoren gespeichert: {total_actors}") - logger.info(f"Sensoren gespeichert: {total_sensors}") - logger.info("=" * 60) - - logger.info("\n✓ Import erfolgreich abgeschlossen!") - - except Exception as e: - logger.error(f"✗ Fehler: {e}") - import traceback - traceback.print_exc() - - finally: - db.disconnect() - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +""" +Device Discovery - Refactored with Unified Architecture +======================================================= +Zentrale Datenbank-Logik im Hauptscript +Einheitliche Schnittstelle für alle Module +""" + +import pymysql +from pymysql import Error +import json +import logging +import configparser +import os +from typing import Optional, List, Dict + +# Import Module +from modules.tahoma_module import TahomaModule +from modules.wled_module import WLEDModule +from modules.mqtt_module import MQTTModule +from modules.shelly_module import ShellyModule +from modules.gartenwasser_module import GartenwasserModule +from modules.logic_module import LogicModule #nicht Vergessen neue Module auch in Zeile 546 einzufügen!! + +# Logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +# ============================================================================ +# CONFIG CLASS (Original) +# ============================================================================ + +class Config: + """Lädt und verwaltet die Konfiguration""" + + def __init__(self, config_file: str = 'config.ini'): + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, config_file) + + if not os.path.exists(config_path): + raise FileNotFoundError( + f"Konfigurationsdatei '{config_file}' nicht gefunden in {script_dir}" + ) + + self.config = configparser.ConfigParser() + self.config.read(config_path, encoding='utf-8') + logger.info(f"Konfiguration geladen von: {config_path}") + self._validate() + + def _validate(self): + required_sections = ['database', 'options'] + for section in required_sections: + if not self.config.has_section(section): + raise ValueError(f"Erforderliche Sektion '[{section}]' fehlt") + + def _get_bool(self, section: str, key: str, fallback: bool = False) -> bool: + value = self.config.get(section, key, fallback=str(fallback)).strip().lower() + return value in ('true', '1', 'yes', 'on') + + def _get_int(self, section: str, key: str, fallback: int = 0) -> int: + try: + return self.config.getint(section, key, fallback=fallback) + except ValueError: + return fallback + + def _get_list(self, section: str, key: str) -> List[str]: + value = self.config.get(section, key, fallback='').strip() + if not value: + return [] + return [item.strip() for item in value.split(',') if item.strip()] + + # Database + @property + def db_host(self) -> str: + return self.config.get('database', 'host').strip() + + @property + def db_port(self) -> int: + return self._get_int('database', 'port', 3306) + + @property + def db_name(self) -> str: + return self.config.get('database', 'database').strip() + + @property + def db_user(self) -> str: + return self.config.get('database', 'user').strip() + + @property + def db_password(self) -> str: + return self.config.get('database', 'password').strip() + + # Tahoma + @property + def tahoma_enable(self) -> bool: + if not self.config.has_section('tahoma'): + return False + return self._get_bool('tahoma', 'enable', False) + + @property + def tahoma_ip(self) -> str: + if not self.config.has_section('tahoma'): + return '' + return self.config.get('tahoma', 'ip', fallback='').strip() + + @property + def tahoma_token(self) -> str: + if not self.config.has_section('tahoma'): + return '' + return self.config.get('tahoma', 'token', fallback='').strip() + + @property + def tahoma_timeout(self) -> int: + if not self.config.has_section('tahoma'): + return 10 + return self._get_int('tahoma', 'timeout', 10) + + # WLED + @property + def wled_enable(self) -> bool: + if not self.config.has_section('wled'): + return False + return self._get_bool('wled', 'enable', False) + + @property + def wled_discovery_timeout(self) -> int: + if not self.config.has_section('wled'): + return 5 + return self._get_int('wled', 'discovery_timeout', 5) + + @property + def wled_manual_ips(self) -> List[str]: + if not self.config.has_section('wled'): + return [] + return self._get_list('wled', 'manual_ips') + + @property + def wled_timeout(self) -> int: + if not self.config.has_section('wled'): + return 2 + return self._get_int('wled', 'timeout', 2) + + # LOGIC + @property + def logic_enable(self) -> bool: + if not self.config.has_section('logic'): + return False + return self._get_bool('logic', 'enable', False) + + # GARTENWASSER + @property + def gartenwasser_enable(self) -> bool: + if not self.config.has_section('gartenwasser'): + return False + return self._get_bool('gartenwasser', 'enable', False) + + + # MQTT + @property + def mqtt_enable(self) -> bool: + if not self.config.has_section('mqtt'): + return False + return self._get_bool('mqtt', 'enable', False) + + @property + def mqtt_broker(self) -> str: + if not self.config.has_section('mqtt'): + return 'localhost' + return self.config.get('mqtt', 'broker', fallback='localhost').strip() + + @property + def mqtt_port(self) -> int: + if not self.config.has_section('mqtt'): + return 1883 + return self._get_int('mqtt', 'port', 1883) + + @property + def mqtt_username(self) -> Optional[str]: + if not self.config.has_section('mqtt'): + return None + value = self.config.get('mqtt', 'username', fallback='').strip() + return value if value else None + + @property + def mqtt_password(self) -> Optional[str]: + if not self.config.has_section('mqtt'): + return None + value = self.config.get('mqtt', 'password', fallback='').strip() + return value if value else None + + @property + def mqtt_discovery_prefix(self) -> str: + if not self.config.has_section('mqtt'): + return 'homeassistant' + return self.config.get('mqtt', 'discovery_prefix', fallback='homeassistant').strip() + + @property + def mqtt_discovery_timeout(self) -> int: + if not self.config.has_section('mqtt'): + return 10 + return self._get_int('mqtt', 'discovery_timeout', 10) + + # Shelly + @property + def shelly_enable(self) -> bool: + if not self.config.has_section('shelly'): + return False + return self._get_bool('shelly', 'enable', False) + + @property + def shelly_network_range(self) -> str: + if not self.config.has_section('shelly'): + return '192.168.1' + return self.config.get('shelly', 'network_range', fallback='192.168.1').strip() + + @property + def shelly_start_ip(self) -> int: + if not self.config.has_section('shelly'): + return 1 + return self._get_int('shelly', 'start_ip', 1) + + @property + def shelly_end_ip(self) -> int: + if not self.config.has_section('shelly'): + return 254 + return self._get_int('shelly', 'end_ip', 254) + + # Options + @property + def clear_tables(self) -> bool: + return self._get_bool('options', 'clear_tables', True) + + @property + def log_level(self) -> str: + level = self.config.get('options', 'log_level', fallback='INFO').strip().upper() + valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + return level if level in valid_levels else 'INFO' + + @property + def log_file(self) -> Optional[str]: + value = self.config.get('options', 'log_file', fallback='').strip() + return value if value else None + + +# ============================================================================ +# DATABASE MANAGER (Zentral im Hauptscript) +# ============================================================================ + +class DatabaseManager: + """Zentrale Datenbank-Verwaltung - ALLE DB-Operationen hier""" + + PARAMETER_TYPES = { + 0: "bool", + 1: "integer", # Integer value + 2: "float", # Floating point number + 3: "string", # Text string + 4: "undefined", + 5: "time", + 6: "array", + 7: "deltatime", + 8: "date", + 9: "datetime", + } + + PARAMETER_TYPES_MAPPING = { + "bool": 0, + "boolean" : 0, + "integer" : 1, # Integer value + "float" : 2, # Floating point number + "number" : 2, # Floating point number + "string" : 3, # Text string + "time" : 5, + "array": 6, + "deltatime" : 7, + "date" : 8, + "datetime" : 9, + } + + def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306): + self.host = host + self.database = database + self.user = user + self.password = password + self.port = port + self.connection = None + + def connect(self) -> bool: + """Stellt Verbindung zur Datenbank her""" + try: + self.connection = pymysql.connect( + host=self.host, + database=self.database, + user=self.user, + password=self.password, + port=self.port, + charset='utf8mb4' + ) + logger.info("✓ Erfolgreich mit MySQL-Datenbank verbunden") + return True + except Error as e: + logger.error(f"✗ Datenbankverbindung fehlgeschlagen: {e}") + return False + + def disconnect(self): + """Schließt Datenbankverbindung""" + if self.connection: + self.connection.close() + logger.info("Datenbankverbindung geschlossen") + + def clear_tables(self): + """Löscht alle Einträge aus allen Tabellen""" + try: + cursor = self.connection.cursor() + cursor.execute("SET FOREIGN_KEY_CHECKS=0") + cursor.execute("TRUNCATE state_types") + cursor.execute("TRUNCATE command_parameters") + cursor.execute("TRUNCATE actor_commands") + cursor.execute("TRUNCATE actor_states") + cursor.execute("TRUNCATE actors") + cursor.execute("TRUNCATE sensor_states") + cursor.execute("TRUNCATE sensors") + + cursor.execute("SET FOREIGN_KEY_CHECKS=1") + + self.connection.commit() + logger.info("✓ Alle Tabellen geleert") + cursor.close() + except Error as e: + logger.error(f"✗ Fehler beim Leeren der Tabellen: {e}") + self.connection.rollback() + + + + def insert_actor(self, device_type: str, name: str, url: str, + commands: list, states: list) -> bool: + """ + Fügt einen Aktor in die Datenbank ein + + Args: + device_type: Typ des Geräts + name: Name des Geräts + url: Eindeutige URL/ID + commands: Liste von Command-Dicts + states: Liste von State-Dicts + """ + try: + cursor = self.connection.cursor() + + # Actor einfügen + query = """ + INSERT INTO actors (type, name, url) + VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE + type = VALUES(type), + name = VALUES(name) + """ + cursor.execute(query, (device_type, name, url)) + query = """ + SELECT ID FROM actors where url = %s + """ + cursor.execute(query, (url)) + actor_id = cursor.fetchone()[0] + + # Commands einfügen + for cmd in commands: + command_name = cmd.get('command', '') + command_url = cmd.get('url', '') + cmd_query = """ + INSERT INTO actor_commands (actor_id, command_name, command_url) + VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE + command_url = VALUES(command_url) + """ + cursor.execute(cmd_query, (actor_id, command_name, command_url)) + query = """ + SELECT ID FROM actor_commands where actor_id = %s and command_name = %s + """ + cursor.execute(query, (actor_id, command_name)) + command_id = cursor.fetchone()[0] + + # Parameter einfügen + for param in cmd.get('parameters', []): + param_query = """ + INSERT INTO command_parameters + (command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + parameter_type = VALUES(parameter_type), + min_value = VALUES(min_value), + max_value = VALUES(max_value), + possible_values = VALUES(possible_values), + url = VALUES(url) + """ + param_name = param.get('name', '') + param_type = self.PARAMETER_TYPES_MAPPING.get(param.get('type', ''), 4) + min_val = param.get('min') + max_val = param.get('max') + possible_vals = json.dumps(param.get('values')) if 'values' in param else "" + param_url = param.get('url') + + cursor.execute(param_query, + (command_id, param_name, param_type, min_val, max_val, possible_vals, param_url)) + + # States einfügen + for state in states: + state_query = """ + INSERT INTO actor_states + (actor_id, state_name, state_type, current_value, unit, url, possible_values, value_path) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + state_type = VALUES(state_type), + -- COALESCE und nicht VALUES() allein: ein Modul, das + -- keinen Startwert kennt, liefert hier NULL, und das + -- loeschte bisher den laufenden Messwert. Der Runner + -- schrieb ihn auch nicht zurueck - er merkt sich, was er + -- zuletzt in die Datenbank geschrieben hat, und der + -- naechste gleiche Wert galt damit als unveraendert. Im + -- Editor stand danach "Regen heute (= )", bis sich der + -- Wert zufaellig einmal aenderte. + current_value = COALESCE(VALUES(current_value), current_value), + possible_values = VALUES(possible_values), + unit = VALUES(unit), + url = VALUES(url), + value_path = VALUES(value_path) + """ + state_name = state.get('name', '') + state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4) + current_value = str(state.get('current_value', '')) if 'current_value' in state else None + unit = state.get('unit') + state_url = state.get('url') + value_path = state.get('value_path') + possible_vals = json.dumps(state.get('values')) if 'values' in state else "" + cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url, possible_vals, value_path)) + + self.connection.commit() + cursor.close() + return True + + except Error as e: + logger.error(f"✗ Fehler beim Einfügen des Aktors {name}: {e}") + self.connection.rollback() + return False + + def fillVariableTypes(self): + cursor = self.connection.cursor() + query = """ + INSERT INTO state_types (id, type) + VALUES (%s, %s) + ON DUPLICATE KEY UPDATE + type = VALUES(type) + """ + for id, parameter_type in self.PARAMETER_TYPES.items(): + cursor.execute(query, (id, parameter_type)) + + def insert_sensor(self, device_type: str, name: str, url: str, states: list) -> bool: + """ + Fügt einen Sensor in die Datenbank ein + + Args: + device_type: Typ des Sensors + name: Name des Sensors + url: Eindeutige URL/ID + states: Liste von State-Dicts + """ + try: + cursor = self.connection.cursor() + + # Sensor einfügen + query = """ + INSERT INTO actors (type, name, url) + VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE + type = VALUES(type), + name = VALUES(name) + """ + cursor.execute(query, (device_type, name, url)) + sensor_id = cursor.lastrowid + query = """ + SELECT ID FROM actors where url = %s + """ + cursor.execute(query, (url)) + sensor_id = cursor.fetchone()[0] + + # States einfügen + for state in states: + state_query = """ + INSERT INTO actor_states + (actor_id, state_name, state_type, current_value, unit, url, value_path, possible_values) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + state_type = VALUES(state_type), + -- Siehe insert_actor: NULL heisst "kein Startwert", + -- nicht "Messwert loeschen". + current_value = COALESCE(VALUES(current_value), current_value), + unit = VALUES(unit), + url = VALUES(url), + value_path = VALUES(value_path), + possible_values = VALUES(possible_values) + """ + state_name = state.get('name', '') + state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4) + current_value = str(state.get('current_value', '')) if 'current_value' in state else None + unit = state.get('unit') + state_url = state.get('url') + value_path = state.get('value_path') + possible_vals = json.dumps(state.get('values')) if 'values' in state else "" + + cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url, value_path, possible_vals)) + + self.connection.commit() + cursor.close() + return True + + except Error as e: + logger.error(f"✗ Fehler beim Einfügen des Sensors {name}: {e}") + self.connection.rollback() + return False + + +# ============================================================================ +# MAIN FUNCTION +# ============================================================================ + +def main(): + """Hauptfunktion - orchestriert alle Module mit einheitlicher Schnittstelle""" + + # Konfiguration laden + try: + config = Config('config.ini') + logger.info("✓ Konfiguration erfolgreich geladen") + except FileNotFoundError as e: + print(f"FEHLER: {e}") + return + except Exception as e: + print(f"FEHLER: {e}") + return + + # Logging anpassen + log_level = getattr(logging, config.log_level) + logger.setLevel(log_level) + + if config.log_file: + file_handler = logging.FileHandler(config.log_file, encoding='utf-8') + file_handler.setLevel(log_level) + file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logger.addHandler(file_handler) + logger.info(f"Logging in Datei: {config.log_file}") + + # Datenbank initialisieren + logger.info("Verbinde mit MySQL-Datenbank...") + db = DatabaseManager( + config.db_host, + config.db_name, + config.db_user, + config.db_password, + config.db_port + ) + + if not db.connect(): + logger.error("Datenbankverbindung fehlgeschlagen. Abbruch.") + return + + try: + # Tabellen leeren + if config.clear_tables: + db.clear_tables() + + total_actors = 0 + total_sensors = 0 + + db.fillVariableTypes() + # Module initialisieren + modules = [ + TahomaModule(config), + WLEDModule(config), + MQTTModule(config), + ShellyModule(config), + LogicModule(config), + GartenwasserModule(config) + ] + + # Jedes Modul durchlaufen + for module in modules: + if not module.is_enabled(): + logger.info(f"Modul {module.get_name()} ist deaktiviert") + continue + + try: + # Discovery durchführen (einheitliche Schnittstelle!) + actors, sensors = module.discover() + + # Actors in DB speichern + for actor in actors: + if db.insert_actor( + actor['type'], + actor['name'], + actor['url'], + actor.get('commands', []), + actor.get('states', []) + ): + total_actors += 1 + logger.info(f" ✓ Actor: {actor['name']} ({actor['type']})") + + # Sensors in DB speichern + for sensor in sensors: + if db.insert_sensor( + sensor['type'], + sensor['name'], + sensor['url'], + sensor.get('states', []) + ): + total_sensors += 1 + logger.info(f" ✓ Sensor: {sensor['name']} ({sensor['type']})") + + except Exception as e: + logger.error(f"✗ Fehler bei Modul {module.get_name()}: {e}") + import traceback + traceback.print_exc() + + # Zusammenfassung + logger.info("\n" + "=" * 60) + logger.info("ZUSAMMENFASSUNG") + logger.info("=" * 60) + logger.info(f"Aktoren gespeichert: {total_actors}") + logger.info(f"Sensoren gespeichert: {total_sensors}") + logger.info("=" * 60) + + logger.info("\n✓ Import erfolgreich abgeschlossen!") + + except Exception as e: + logger.error(f"✗ Fehler: {e}") + import traceback + traceback.print_exc() + + finally: + db.disconnect() + + +if __name__ == "__main__": + main() diff --git a/restricted/deviceDiscovery/modules/base_module.py b/restricted/deviceDiscovery/modules/base_module.py index bede186..daf4a09 100644 --- a/restricted/deviceDiscovery/modules/base_module.py +++ b/restricted/deviceDiscovery/modules/base_module.py @@ -1,95 +1,95 @@ -#!/usr/bin/env python3 -""" -Base Module Interface -Definiert die einheitliche Schnittstelle für alle Gerätemodule -""" - -from abc import ABC, abstractmethod -from typing import List, Dict, Tuple -import logging - -logger = logging.getLogger(__name__) - - -class BaseModule(ABC): - """ - Abstrakte Basisklasse für alle Gerätemodule - - Jedes Modul muss nur discover() implementieren und gibt - eine Liste von (actors, sensors) zurück. - Die Datenbank-Logik bleibt im Hauptscript. - """ - - def __init__(self, config): - """ - Initialisiert das Modul - - Args: - config: Config-Objekt mit allen Einstellungen - """ - self.config = config - self.module_name = self.__class__.__name__.replace('Module', '') - - @abstractmethod - def discover(self) -> Tuple[List[Dict], List[Dict]]: - """ - Führt Device Discovery durch - - Returns: - Tuple (actors, sensors) mit Listen von Dicts: - - Actor Dict Format: - { - 'type': str, # z.B. 'RollerShutter' - 'name': str, # z.B. 'Wohnzimmer Rollo' - 'url': str, # Eindeutige ID/URL - 'commands': [ # Liste von Commands - { - 'command': str, - 'parameters': [ - { - 'name': str, - 'type': str, - 'min': float (optional), - 'max': float (optional), - 'url': str, # Eindeutige ID/URL - 'values': list (optional) - } - ] - } - ], - 'states': [ # Liste von States - { - 'name': str, - 'type': int/str, - 'current_value': any, - 'unit': str (optional) - } - ] - } - - Sensor Dict Format: - { - 'type': str, # z.B. 'TemperatureSensor' - 'name': str, # z.B. 'Außentemperatur' - 'url': str, # Eindeutige ID/URL - 'states': [ # Liste von States - { - 'name': str, - 'type': int/str, - 'current_value': any, - 'url': str, # Eindeutige ID/URL - 'unit': str (optional) - } - ] - } - """ - pass - - def is_enabled(self) -> bool: - """Prüft ob Modul aktiviert ist""" - return True # Override in Subklassen falls nötig - - def get_name(self) -> str: - """Gibt Modulname zurück""" - return self.module_name +#!/usr/bin/env python3 +""" +Base Module Interface +Definiert die einheitliche Schnittstelle für alle Gerätemodule +""" + +from abc import ABC, abstractmethod +from typing import List, Dict, Tuple +import logging + +logger = logging.getLogger(__name__) + + +class BaseModule(ABC): + """ + Abstrakte Basisklasse für alle Gerätemodule + + Jedes Modul muss nur discover() implementieren und gibt + eine Liste von (actors, sensors) zurück. + Die Datenbank-Logik bleibt im Hauptscript. + """ + + def __init__(self, config): + """ + Initialisiert das Modul + + Args: + config: Config-Objekt mit allen Einstellungen + """ + self.config = config + self.module_name = self.__class__.__name__.replace('Module', '') + + @abstractmethod + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Führt Device Discovery durch + + Returns: + Tuple (actors, sensors) mit Listen von Dicts: + + Actor Dict Format: + { + 'type': str, # z.B. 'RollerShutter' + 'name': str, # z.B. 'Wohnzimmer Rollo' + 'url': str, # Eindeutige ID/URL + 'commands': [ # Liste von Commands + { + 'command': str, + 'parameters': [ + { + 'name': str, + 'type': str, + 'min': float (optional), + 'max': float (optional), + 'url': str, # Eindeutige ID/URL + 'values': list (optional) + } + ] + } + ], + 'states': [ # Liste von States + { + 'name': str, + 'type': int/str, + 'current_value': any, + 'unit': str (optional) + } + ] + } + + Sensor Dict Format: + { + 'type': str, # z.B. 'TemperatureSensor' + 'name': str, # z.B. 'Außentemperatur' + 'url': str, # Eindeutige ID/URL + 'states': [ # Liste von States + { + 'name': str, + 'type': int/str, + 'current_value': any, + 'url': str, # Eindeutige ID/URL + 'unit': str (optional) + } + ] + } + """ + pass + + def is_enabled(self) -> bool: + """Prüft ob Modul aktiviert ist""" + return True # Override in Subklassen falls nötig + + def get_name(self) -> str: + """Gibt Modulname zurück""" + return self.module_name diff --git a/restricted/deviceDiscovery/modules/gartenwasser_module.py b/restricted/deviceDiscovery/modules/gartenwasser_module.py index b27ff7d..b297727 100644 --- a/restricted/deviceDiscovery/modules/gartenwasser_module.py +++ b/restricted/deviceDiscovery/modules/gartenwasser_module.py @@ -1,238 +1,238 @@ -#!/usr/bin/env python3 -""" -Gartenwasser Module -Die beiden Ventilsteuerungen und der Regen - fest beschrieben, nicht gesucht -KEINE Datenbank-Operationen! -""" - -import logging -from typing import List, Dict, Tuple -from modules.base_module import BaseModule - -logger = logging.getLogger(__name__) - -# Topic, auf das gatherRainData.py im SolarManager seine Regensummen legt. -# Dasselbe Topic steht dort noch einmal im Quelltext. Bewusst kein -# Konfigurationseintrag: er müsste in beiden Repositorien gleich lauten, und -# eine Einstellung, die man an zwei Orten gleich halten muss, ist keine. -REGEN_TOPIC = "Wetter/Regen" - -# Welche Regenfenster als Messwert im Editor auftauchen. gatherRainData.py -# veröffentlicht immer tage1 bis tage7; welche davon jemand braucht, entscheidet -# allein diese Liste. Die drei Zonen der alten auto_watering.py brauchten 1, 2 -# und 5 Tage - der Rest steht da, damit eine neue Zone keinen Eingriff im -# SolarManager kostet. -REGEN_FENSTER = [1, 2, 3, 5, 7] - -# Die beiden ESP32-Ventilsteuerungen. Sie melden sich nicht per -# Home-Assistant-Discovery, das MQTT-Modul findet sie also nicht. -# -# topic Wurzel aller Topics dieser Steuerung -# modi Nutzlast des /auto-Topics -> Beschriftung im Editor. Dieselben -# Klartexte stehen in der Whitelist von ajax/watering.php. -# zonen Ventilnummer -> Name der Zone. Eine Steuerung bedient mehrere -# Zonen; "vorn" zum Beispiel Ventil 1 für die Tröge und Ventil 6 -# für den Garten. Aus jeder Zone werden zwei Messwerte. -STEUERUNGEN = [ - { - "name": "Gartenwasser vorn", - "topic": "Gartenwasser/vorn", - "modi": [{"Trog": "Tröge"}, {"Vorn": "Garten vorn"}, {"Stop": "Stopp"}], - "zonen": [(1, "Tröge"), (6, "Garten vorn")], - }, - { - "name": "Gartenwasser hinten", - "topic": "Gartenwasser/hinten", - "modi": [{"Hoch": "Hochbeete"}, {"Stop": "Stopp"}], - "zonen": [(6, "Hochbeete")], - }, -] - - -class GartenwasserModule(BaseModule): - """ - Gartenwasser Modul - Implementiert BaseModule Interface - - Anders als die übrigen Module sucht dieses nichts, es schreibt drei feste - Geräte hin - wie das Logic-Modul auch: - - * Die Ventilsteuerungen sprechen MQTT, aber ohne Home-Assistant- - Discovery. Ohne dieses Modul kennt die Datenbank sie nicht, und im - Automatik-Editor lässt sich die Bewässerung nicht auswählen. - * "Regen" ist gar kein Gerät, sondern das, was gatherRainData.py im - SolarManager von Open-Meteo holt. - - Von Hand in die Datenbank geschrieben würden die Zeilen zwar jeden - Discovery-Lauf überleben (es wird nur mit ON DUPLICATE KEY UPDATE - geschrieben, nie gelöscht), aber niemand wüsste mehr, woher sie kommen. - - Zusammen ersetzt das restricted/gartenbewaesserung/auto_watering.py: die - Zeitfenster werden Bedingungen auf Sonnenauf- und -untergang, die - Regenschwellen Bedingungen auf "Regen ... Tage", und geschaltet wird über - das Kommando "Automatik". - - Zwei Zusagen der Ventilsteuerung stecken in den Messwerten unten, und ohne - sie stimmt die Automatik nicht mehr: - - * LastWatering trägt je Ventil ein "daysSinceWatering" - die Anzahl - Tage, nicht den Zeitstempel. Das Feld fehlt, solange für ein Ventil - keine Bewässerung bekannt ist. - * WateringToday wird um Mitternacht zurückgesetzt. - * Running sagt retained, ob gerade etwas läuft, und fällt über eine - Last-Will-Nachricht auch dann auf false, wenn die Steuerung wegbricht. - """ - - def is_enabled(self) -> bool: - """Prüft ob Gartenwasser aktiviert ist""" - return self.config.gartenwasser_enable - - def discover(self) -> Tuple[List[Dict], List[Dict]]: - """ - Erzeugt die beiden Ventilsteuerungen als Actors und den Regen als Sensor - - Returns: - Tuple (actors, sensors) - """ - logger.info("\n" + "=" * 60) - logger.info("GARTENWASSER-GERÄTE WERDEN ERZEUGT") - logger.info("=" * 60) - - actors = [self._steuerung(s) for s in STEUERUNGEN] - sensors = [self._regen()] - return actors, sensors - - @staticmethod - def _steuerung(steuerung: Dict) -> Dict: - """ - Eine Ventilsteuerung: ein Kommando, zwei Messwerte je Zone. - - Das Kommando trägt seinen Klartext im Parameter und nicht im Namen. - Ein Kommando ohne Parameter schickt im Runner eine leere Nutzlast - (siehe MQTTTransport.senden), der Modus muss also der Parameter sein - - im Editor liest sich das dann als "Automatik Modus = Hochbeete". - """ - topic = steuerung["topic"] - - # Läuft an dieser Steuerung gerade etwas? Der Wächter für jede Regel, - # die hier etwas anstoßen will - die Zonen einer Steuerung hängen an - # derselben Leitung, zwei gleichzeitig gibt es nicht. - # - # Gelesen wird das aus Running und nicht aus Timers: Timers ist nicht - # retained und wird nur im Sekundentakt gesendet, solange etwas läuft. - # Der Runner sähe im Ruhezustand gar nichts und behielte nach einem - # Lauf den zuletzt gesehenen Wert - also genau das Gegenteil von - # verlässlich. Running ist retained und fällt über eine - # Last-Will-Nachricht auch dann auf false, wenn die Steuerung mitten - # im Gießen wegbricht. - states = [{ - "name": "Bewässerung läuft", - "url": topic + "/Running", - "value_path": "running", - "type": "bool", - }, { - # Klartext der laufenden Automatik ("Hoch", "Trog", "Vorn"), leer - # wenn von Hand oder gar nicht bewässert wird. - "name": "Laufender Modus", - "url": topic + "/Running", - "value_path": "mode", - "type": "string", - }] - - for valve, zone in steuerung["zonen"]: - if len(steuerung["zonen"]) > 1: - # Bei einer Steuerung mit nur einer Zone wäre das derselbe Wert - # wie "Bewässerung läuft" - ein zweiter Messwert, der nie etwas - # anderes sagt, macht die Auswahl nur länger. - states.append({ - "name": "Bewässerung läuft " + zone, - "url": topic + "/Running", - "value_path": "running%d" % valve, - "type": "bool", - }) - states.append({ - # Ersetzt die Zwölf-Stunden-Sperre von auto_watering.py: - # "heute noch nicht bewässert" heißt hier schlicht "= 0". - # - # Dass das trägt, liegt an der Steuerung: sie setzt den Wert um - # Mitternacht zurück. Die Nachricht ist retained, und der Runner - # sieht nur die Zahl hinter dem value_path - den Tag, den die - # Nutzlast als "wateringDay" mitführt, kann er nicht prüfen. - # Ohne den Reset stünde nach einer bewässerungsfreien Nacht die - # Summe von gestern als "heute" da, und die Zone bliebe trocken. - "name": "Bewässert heute " + zone, - "url": topic + "/WateringToday", - "value_path": "wateringDurationTodaySecs%d" % valve, - "type": "integer", - "unit": "s", - }) - states.append({ - # Der Trockenheits-Notlauf (max_dry_days in auto_watering.py). - # Ein Zeitstempel nützte hier nichts: der Runner vergleicht - # Datumsangaben nur gegen einen festen Wert, "länger als fünf - # Tage her" ist damit nicht formulierbar. Die Steuerung liefert - # deshalb gleich die Anzahl Tage - dafür wurde ihre Firmware - # erweitert, das Feld gab es vorher nicht. - # - # Für ein Ventil, das noch nie lief, lässt sie das Feld weg - # statt eine 0 zu schicken. Eine 0 hieße "heute bewässert" und - # würde den Notlauf für immer unterdrücken; ohne Feld bleibt - # der Messwert leer, und die Bedingung greift schlicht nicht. - "name": "Tage seit Bewässerung " + zone, - "url": topic + "/LastWatering", - "value_path": "daysSinceWatering%d" % valve, - "type": "integer", - "unit": "Tage", - }) - - return { - "type": "Bewässerung", - "name": steuerung["name"], - "url": "mqtt://" + topic, - "commands": [{ - "command": "Automatik", - "url": topic + "/auto", - "parameters": [{ - "name": "Modus", - "type": "string", - "url": topic + "/auto", - "values": steuerung["modi"], - }], - }], - "states": states, - } - - @staticmethod - def _regen() -> Dict: - """ - Der Regen als Gerät: je Zeitraum ein Messwert, dazu der Stand. - - "Regen heute" ist der bereits gefallene Regen des laufenden Tages, die - übrigen zählen ihn plus die abgeschlossenen Tage davor - so hat auch - auto_watering.py gerechnet. - - Der Stand ist kein Messwert zum Vergleichen, sondern zum Nachsehen: im - Editor steht er hinter dem Namen ("= 04.09.2026 09:00:00") und verrät, - ob der Sammler noch läuft. Bleibt er länger aus, leert gatherRainData.py - die Zahlen von sich aus, und die Bewässerung setzt aus. - """ - states = [{ - "name": "Regen heute" if tage == 1 else "Regen %d Tage" % tage, - "url": REGEN_TOPIC, - "value_path": "tage%d" % tage, - "type": "float", - "unit": "mm", - } for tage in REGEN_FENSTER] - - states.append({ - "name": "Stand", - "url": REGEN_TOPIC, - "value_path": "stand", - "type": "string", - }) - - return { - "type": "Wetter", - "name": "Regen", - "url": "mqtt://" + REGEN_TOPIC, - "states": states, - } +#!/usr/bin/env python3 +""" +Gartenwasser Module +Die beiden Ventilsteuerungen und der Regen - fest beschrieben, nicht gesucht +KEINE Datenbank-Operationen! +""" + +import logging +from typing import List, Dict, Tuple +from modules.base_module import BaseModule + +logger = logging.getLogger(__name__) + +# Topic, auf das gatherRainData.py im SolarManager seine Regensummen legt. +# Dasselbe Topic steht dort noch einmal im Quelltext. Bewusst kein +# Konfigurationseintrag: er müsste in beiden Repositorien gleich lauten, und +# eine Einstellung, die man an zwei Orten gleich halten muss, ist keine. +REGEN_TOPIC = "Wetter/Regen" + +# Welche Regenfenster als Messwert im Editor auftauchen. gatherRainData.py +# veröffentlicht immer tage1 bis tage7; welche davon jemand braucht, entscheidet +# allein diese Liste. Die drei Zonen der alten auto_watering.py brauchten 1, 2 +# und 5 Tage - der Rest steht da, damit eine neue Zone keinen Eingriff im +# SolarManager kostet. +REGEN_FENSTER = [1, 2, 3, 5, 7] + +# Die beiden ESP32-Ventilsteuerungen. Sie melden sich nicht per +# Home-Assistant-Discovery, das MQTT-Modul findet sie also nicht. +# +# topic Wurzel aller Topics dieser Steuerung +# modi Nutzlast des /auto-Topics -> Beschriftung im Editor. Dieselben +# Klartexte stehen in der Whitelist von ajax/watering.php. +# zonen Ventilnummer -> Name der Zone. Eine Steuerung bedient mehrere +# Zonen; "vorn" zum Beispiel Ventil 1 für die Tröge und Ventil 6 +# für den Garten. Aus jeder Zone werden zwei Messwerte. +STEUERUNGEN = [ + { + "name": "Gartenwasser vorn", + "topic": "Gartenwasser/vorn", + "modi": [{"Trog": "Tröge"}, {"Vorn": "Garten vorn"}, {"Stop": "Stopp"}], + "zonen": [(1, "Tröge"), (6, "Garten vorn")], + }, + { + "name": "Gartenwasser hinten", + "topic": "Gartenwasser/hinten", + "modi": [{"Hoch": "Hochbeete"}, {"Stop": "Stopp"}], + "zonen": [(6, "Hochbeete")], + }, +] + + +class GartenwasserModule(BaseModule): + """ + Gartenwasser Modul - Implementiert BaseModule Interface + + Anders als die übrigen Module sucht dieses nichts, es schreibt drei feste + Geräte hin - wie das Logic-Modul auch: + + * Die Ventilsteuerungen sprechen MQTT, aber ohne Home-Assistant- + Discovery. Ohne dieses Modul kennt die Datenbank sie nicht, und im + Automatik-Editor lässt sich die Bewässerung nicht auswählen. + * "Regen" ist gar kein Gerät, sondern das, was gatherRainData.py im + SolarManager von Open-Meteo holt. + + Von Hand in die Datenbank geschrieben würden die Zeilen zwar jeden + Discovery-Lauf überleben (es wird nur mit ON DUPLICATE KEY UPDATE + geschrieben, nie gelöscht), aber niemand wüsste mehr, woher sie kommen. + + Zusammen ersetzt das restricted/gartenbewaesserung/auto_watering.py: die + Zeitfenster werden Bedingungen auf Sonnenauf- und -untergang, die + Regenschwellen Bedingungen auf "Regen ... Tage", und geschaltet wird über + das Kommando "Automatik". + + Zwei Zusagen der Ventilsteuerung stecken in den Messwerten unten, und ohne + sie stimmt die Automatik nicht mehr: + + * LastWatering trägt je Ventil ein "daysSinceWatering" - die Anzahl + Tage, nicht den Zeitstempel. Das Feld fehlt, solange für ein Ventil + keine Bewässerung bekannt ist. + * WateringToday wird um Mitternacht zurückgesetzt. + * Running sagt retained, ob gerade etwas läuft, und fällt über eine + Last-Will-Nachricht auch dann auf false, wenn die Steuerung wegbricht. + """ + + def is_enabled(self) -> bool: + """Prüft ob Gartenwasser aktiviert ist""" + return self.config.gartenwasser_enable + + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Erzeugt die beiden Ventilsteuerungen als Actors und den Regen als Sensor + + Returns: + Tuple (actors, sensors) + """ + logger.info("\n" + "=" * 60) + logger.info("GARTENWASSER-GERÄTE WERDEN ERZEUGT") + logger.info("=" * 60) + + actors = [self._steuerung(s) for s in STEUERUNGEN] + sensors = [self._regen()] + return actors, sensors + + @staticmethod + def _steuerung(steuerung: Dict) -> Dict: + """ + Eine Ventilsteuerung: ein Kommando, zwei Messwerte je Zone. + + Das Kommando trägt seinen Klartext im Parameter und nicht im Namen. + Ein Kommando ohne Parameter schickt im Runner eine leere Nutzlast + (siehe MQTTTransport.senden), der Modus muss also der Parameter sein - + im Editor liest sich das dann als "Automatik Modus = Hochbeete". + """ + topic = steuerung["topic"] + + # Läuft an dieser Steuerung gerade etwas? Der Wächter für jede Regel, + # die hier etwas anstoßen will - die Zonen einer Steuerung hängen an + # derselben Leitung, zwei gleichzeitig gibt es nicht. + # + # Gelesen wird das aus Running und nicht aus Timers: Timers ist nicht + # retained und wird nur im Sekundentakt gesendet, solange etwas läuft. + # Der Runner sähe im Ruhezustand gar nichts und behielte nach einem + # Lauf den zuletzt gesehenen Wert - also genau das Gegenteil von + # verlässlich. Running ist retained und fällt über eine + # Last-Will-Nachricht auch dann auf false, wenn die Steuerung mitten + # im Gießen wegbricht. + states = [{ + "name": "Bewässerung läuft", + "url": topic + "/Running", + "value_path": "running", + "type": "bool", + }, { + # Klartext der laufenden Automatik ("Hoch", "Trog", "Vorn"), leer + # wenn von Hand oder gar nicht bewässert wird. + "name": "Laufender Modus", + "url": topic + "/Running", + "value_path": "mode", + "type": "string", + }] + + for valve, zone in steuerung["zonen"]: + if len(steuerung["zonen"]) > 1: + # Bei einer Steuerung mit nur einer Zone wäre das derselbe Wert + # wie "Bewässerung läuft" - ein zweiter Messwert, der nie etwas + # anderes sagt, macht die Auswahl nur länger. + states.append({ + "name": "Bewässerung läuft " + zone, + "url": topic + "/Running", + "value_path": "running%d" % valve, + "type": "bool", + }) + states.append({ + # Ersetzt die Zwölf-Stunden-Sperre von auto_watering.py: + # "heute noch nicht bewässert" heißt hier schlicht "= 0". + # + # Dass das trägt, liegt an der Steuerung: sie setzt den Wert um + # Mitternacht zurück. Die Nachricht ist retained, und der Runner + # sieht nur die Zahl hinter dem value_path - den Tag, den die + # Nutzlast als "wateringDay" mitführt, kann er nicht prüfen. + # Ohne den Reset stünde nach einer bewässerungsfreien Nacht die + # Summe von gestern als "heute" da, und die Zone bliebe trocken. + "name": "Bewässert heute " + zone, + "url": topic + "/WateringToday", + "value_path": "wateringDurationTodaySecs%d" % valve, + "type": "integer", + "unit": "s", + }) + states.append({ + # Der Trockenheits-Notlauf (max_dry_days in auto_watering.py). + # Ein Zeitstempel nützte hier nichts: der Runner vergleicht + # Datumsangaben nur gegen einen festen Wert, "länger als fünf + # Tage her" ist damit nicht formulierbar. Die Steuerung liefert + # deshalb gleich die Anzahl Tage - dafür wurde ihre Firmware + # erweitert, das Feld gab es vorher nicht. + # + # Für ein Ventil, das noch nie lief, lässt sie das Feld weg + # statt eine 0 zu schicken. Eine 0 hieße "heute bewässert" und + # würde den Notlauf für immer unterdrücken; ohne Feld bleibt + # der Messwert leer, und die Bedingung greift schlicht nicht. + "name": "Tage seit Bewässerung " + zone, + "url": topic + "/LastWatering", + "value_path": "daysSinceWatering%d" % valve, + "type": "integer", + "unit": "Tage", + }) + + return { + "type": "Bewässerung", + "name": steuerung["name"], + "url": "mqtt://" + topic, + "commands": [{ + "command": "Automatik", + "url": topic + "/auto", + "parameters": [{ + "name": "Modus", + "type": "string", + "url": topic + "/auto", + "values": steuerung["modi"], + }], + }], + "states": states, + } + + @staticmethod + def _regen() -> Dict: + """ + Der Regen als Gerät: je Zeitraum ein Messwert, dazu der Stand. + + "Regen heute" ist der bereits gefallene Regen des laufenden Tages, die + übrigen zählen ihn plus die abgeschlossenen Tage davor - so hat auch + auto_watering.py gerechnet. + + Der Stand ist kein Messwert zum Vergleichen, sondern zum Nachsehen: im + Editor steht er hinter dem Namen ("= 04.09.2026 09:00:00") und verrät, + ob der Sammler noch läuft. Bleibt er länger aus, leert gatherRainData.py + die Zahlen von sich aus, und die Bewässerung setzt aus. + """ + states = [{ + "name": "Regen heute" if tage == 1 else "Regen %d Tage" % tage, + "url": REGEN_TOPIC, + "value_path": "tage%d" % tage, + "type": "float", + "unit": "mm", + } for tage in REGEN_FENSTER] + + states.append({ + "name": "Stand", + "url": REGEN_TOPIC, + "value_path": "stand", + "type": "string", + }) + + return { + "type": "Wetter", + "name": "Regen", + "url": "mqtt://" + REGEN_TOPIC, + "states": states, + } diff --git a/restricted/deviceDiscovery/modules/logic_module.py b/restricted/deviceDiscovery/modules/logic_module.py index 137a6c7..628e567 100644 --- a/restricted/deviceDiscovery/modules/logic_module.py +++ b/restricted/deviceDiscovery/modules/logic_module.py @@ -1,74 +1,74 @@ -#!/usr/bin/env python3 -""" -WLED Module -Enthält NUR Logik-spezifische Geräte-Discovery Logik -KEINE Datenbank-Operationen! -""" - -import requests -import socket -import concurrent.futures -import logging -from typing import List, Dict, Optional, Tuple -from modules.base_module import BaseModule - -logger = logging.getLogger(__name__) - - -class LogicModule(BaseModule): - """ - Logic Modul - Implementiert BaseModule Interface - Gibt nur Actors zurück, KEINE DB-Operationen - """ - - def is_enabled(self) -> bool: - """Prüft ob Logic aktiviert ist""" - return self.config.logic_enable - - def discover(self) -> Tuple[List[Dict], List[Dict]]: - """ - Erzeugt Einträge für Sensorunabhängige Funktionen (Timer, Sonnenauf/untergang, Uhrzeiten, etc.) - - Returns: - Tuple (actors, sensors) - WLED sind immer Actors - """ - logger.info("\n" + "=" * 60) - logger.info("LOGIC-GERÄTE WERDEN ERZEUGT") - logger.info("=" * 60) - - actors = [] - sensors = [] - - states = [] - states.append({ - 'name': 'Uhrzeit', - 'url': 'time', - 'type': 'time', - 'current_value': '13:45' - }) - states.append({ - 'name': 'Datum', - 'url': 'date', - 'type': 'date', - 'current_value': '20.03.2026' - }) - states.append({ - 'name': 'Sonnenaufgang', - 'url': 'sunrise', - 'type': 'deltatime', - 'current_value': '00:00' - }) - states.append({ - 'name': 'Sonnenuntergang', - 'url': 'sunset', - 'type': 'deltatime', - 'current_value': '00:00' - }) - - sensors.append({'type': 'LOGIC', - 'name': "Zeitpunkt", - 'url': f"Logic", - 'commands': [], - 'states': states}) - - return actors, sensors +#!/usr/bin/env python3 +""" +WLED Module +Enthält NUR Logik-spezifische Geräte-Discovery Logik +KEINE Datenbank-Operationen! +""" + +import requests +import socket +import concurrent.futures +import logging +from typing import List, Dict, Optional, Tuple +from modules.base_module import BaseModule + +logger = logging.getLogger(__name__) + + +class LogicModule(BaseModule): + """ + Logic Modul - Implementiert BaseModule Interface + Gibt nur Actors zurück, KEINE DB-Operationen + """ + + def is_enabled(self) -> bool: + """Prüft ob Logic aktiviert ist""" + return self.config.logic_enable + + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Erzeugt Einträge für Sensorunabhängige Funktionen (Timer, Sonnenauf/untergang, Uhrzeiten, etc.) + + Returns: + Tuple (actors, sensors) - WLED sind immer Actors + """ + logger.info("\n" + "=" * 60) + logger.info("LOGIC-GERÄTE WERDEN ERZEUGT") + logger.info("=" * 60) + + actors = [] + sensors = [] + + states = [] + states.append({ + 'name': 'Uhrzeit', + 'url': 'time', + 'type': 'time', + 'current_value': '13:45' + }) + states.append({ + 'name': 'Datum', + 'url': 'date', + 'type': 'date', + 'current_value': '20.03.2026' + }) + states.append({ + 'name': 'Sonnenaufgang', + 'url': 'sunrise', + 'type': 'deltatime', + 'current_value': '00:00' + }) + states.append({ + 'name': 'Sonnenuntergang', + 'url': 'sunset', + 'type': 'deltatime', + 'current_value': '00:00' + }) + + sensors.append({'type': 'LOGIC', + 'name': "Zeitpunkt", + 'url': f"Logic", + 'commands': [], + 'states': states}) + + return actors, sensors diff --git a/restricted/deviceDiscovery/modules/mqtt_module.py b/restricted/deviceDiscovery/modules/mqtt_module.py index 255a416..3cfc9ec 100644 --- a/restricted/deviceDiscovery/modules/mqtt_module.py +++ b/restricted/deviceDiscovery/modules/mqtt_module.py @@ -1,773 +1,773 @@ -#!/usr/bin/env python3 -""" -MQTT/Home Assistant Discovery Integration -Erweitert das Tahoma Script um MQTT-Geräte via Home Assistant Discovery -""" - -import paho.mqtt.client as mqtt -import json -import re -import time -from typing import Dict, List, Optional -import logging - -logger = logging.getLogger(__name__) - - -class HomeAssistantDiscovery: - """Klasse für Home Assistant MQTT Discovery""" - - # Bekannte Discovery-Komponenten - COMPONENTS = [ - 'binary_sensor', 'sensor', 'switch', 'light', 'cover', - 'climate', 'fan', 'lock', 'camera', 'vacuum', 'alarm_control_panel', - 'device_tracker', 'number', 'select', 'button', 'text' - ] - - def __init__(self, broker: str, port: int = 1883, username: str = None, - password: str = None, discovery_prefix: str = 'homeassistant'): - """ - Initialisiert Home Assistant Discovery - - Args: - broker: MQTT Broker IP/Hostname - port: MQTT Port (Standard: 1883) - username: MQTT Benutzername (optional) - password: MQTT Passwort (optional) - discovery_prefix: Discovery Prefix (Standard: 'homeassistant') - """ - self.broker = broker - self.port = port - self.username = username - self.password = password - self.discovery_prefix = discovery_prefix - self.client = None - self.discovered_devices = {} - - def connect(self) -> bool: - """ - Verbindet mit dem MQTT Broker - - Returns: - True bei Erfolg, False bei Fehler - """ - try: - self.client = mqtt.Client() - - if self.username and self.password: - self.client.username_pw_set(self.username, self.password) - - self.client.on_connect = self._on_connect - self.client.on_message = self._on_message - - self.client.connect(self.broker, self.port, 60) - logger.info(f"Verbunden mit MQTT Broker {self.broker}:{self.port}") - return True - - except Exception as e: - logger.error(f"MQTT Verbindungsfehler: {e}") - return False - - def _on_connect(self, client, userdata, flags, rc): - """Callback wenn Verbindung hergestellt wurde""" - if rc == 0: - logger.info("MQTT Verbindung erfolgreich") - # Alle Discovery Topics abonnieren mit Wildcard für object_id - for component in self.COMPONENTS: - # Unterstützt beide Topic-Formate: - # homeassistant/component/node_id/config (4 Teile) - # homeassistant/component/node_id/object_id/config (5 Teile) - topic = f"{self.discovery_prefix}/{component}/+/+/config" - client.subscribe(topic) - logger.debug(f"Abonniert: {topic}") - # Zusätzlich auch das kürzere Format abonnieren - topic_short = f"{self.discovery_prefix}/{component}/+/config" - client.subscribe(topic_short) - logger.debug(f"Abonniert: {topic_short}") - else: - logger.error(f"MQTT Verbindung fehlgeschlagen, Code: {rc}") - - def _on_message(self, client, userdata, msg): - """Callback wenn Nachricht empfangen wurde""" - try: - # Topic analysieren - unterstützt beide Formate: - # homeassistant/component/node_id/config (4 Teile) - # homeassistant/component/node_id/object_id/config (5 Teile) - topic_parts = msg.topic.split('/') - - if topic_parts[-1] != 'config': - return # Kein Config-Topic - - if len(topic_parts) == 4: - # Format: homeassistant/component/node_id/config - component = topic_parts[1] - node_id = topic_parts[2] - object_id = None - elif len(topic_parts) == 5: - # Format: homeassistant/component/node_id/object_id/config - component = topic_parts[1] - node_id = topic_parts[2] - object_id = topic_parts[3] - else: - logger.debug(f"Unbekanntes Topic-Format: {msg.topic}") - return - - # Payload parsen - if msg.payload: - config = json.loads(msg.payload.decode('utf-8')) - - # Eindeutigen Key erstellen - if object_id: - device_key = f"{component}_{node_id}_{object_id}" - else: - device_key = f"{component}_{node_id}" - - # Gerät speichern - self.discovered_devices[device_key] = { - 'component': component, - 'node_id': node_id, - 'object_id': object_id, - 'config': config, - 'topic': msg.topic - } - - device_name = config.get('device', config.get('dev',{'name': node_id})).get('name',node_id) - logger.debug(f"Gerät gefunden: {device_name} ({component}) - {msg.topic}") - - except Exception as e: - logger.error(f"Fehler beim Verarbeiten der MQTT-Nachricht von {msg.topic}: {e}") - - def discover_devices(self, timeout: int = 10) -> Dict: - """ - Sucht nach Home Assistant Discovery Geräten - - Args: - timeout: Timeout in Sekunden - - Returns: - Dictionary mit gefundenen Geräten - """ - logger.info(f"Starte Home Assistant Discovery (Timeout: {timeout}s)...") - logger.info(f"Lausche auf {self.discovery_prefix}/+/+/+/config und {self.discovery_prefix}/+/+/config") - - self.discovered_devices = {} - - # MQTT Loop starten - self.client.loop_start() - - # Warten auf Nachrichten - mit Fortschrittsanzeige - for i in range(timeout): - time.sleep(1) - if (i + 1) % 5 == 0 or i == timeout - 1: - logger.info(f"Discovery läuft... {len(self.discovered_devices)} Geräte gefunden ({i+1}/{timeout}s)") - - # Loop stoppen - self.client.loop_stop() - - logger.info(f"✓ {len(self.discovered_devices)} MQTT-Geräte gefunden") - - # Debug: Zeige einige gefundene Topics - if self.discovered_devices: - logger.debug("Gefundene Geräte (Auswahl):") - for i, (key, device) in enumerate(list(self.discovered_devices.items())[:5]): - logger.debug(f" - {device['config'].get('name', key)} ({device['component']}) via {device['topic']}") - if len(self.discovered_devices) > 5: - logger.debug(f" ... und {len(self.discovered_devices) - 5} weitere") - - return self.discovered_devices - - def disconnect(self): - """Trennt die MQTT-Verbindung""" - if self.client: - self.client.disconnect() - logger.info("MQTT-Verbindung getrennt") - - -class MQTTDeviceConverter: - """Konvertiert MQTT Discovery Entities in Datenbank-Format, gruppiert nach Gerät""" - - # Mapping von HA Komponenten zu Actor/Sensor - ACTOR_COMPONENTS = ['switch', 'light', 'cover', 'fan', 'lock', 'climate', - 'vacuum', 'alarm_control_panel', ' ', 'number', 'select'] - SENSOR_COMPONENTS = ['binary_sensor', 'sensor', 'device_tracker'] - - @staticmethod - def group_entities_by_device(discovered_devices: Dict) -> Dict[str, List]: - """ - Gruppiert Discovery-Entities nach Gerät (node_id) - - Args: - discovered_devices: Dictionary mit allen gefundenen Entities - - Returns: - Dictionary: {device_id: [entity1, entity2, ...]} - """ - devices = {} - - for entity_key, entity in discovered_devices.items(): - # Device Identifier aus Config extrahieren - config = entity.get('config', {}) - device_info = config.get('device') or config.get('dev') or {} - # Node ID als Geräte-Identifier verwenden - node_id = entity.get('node_id', 'unknown') - - # Zusätzlich Device Identifiers prüfen falls vorhanden - if device_info and 'identifiers' in device_info: - identifiers = device_info['identifiers'] - if isinstance(identifiers, list) and identifiers: - node_id = identifiers[0] - - if node_id not in devices: - devices[node_id] = { - 'entities': [], - 'device_info': device_info, - 'node_id': node_id - } - - devices[node_id]['entities'].append(entity) - - return devices - - @staticmethod - def is_actor_entity(component: str) -> bool: - """Prüft ob Entity-Komponente ein Aktor ist""" - if(component == "climate"): - logger.info("Climate device gefunden"); - return component in MQTTDeviceConverter.ACTOR_COMPONENTS - - @staticmethod - def is_sensor_entity(component: str) -> bool: - """Prüft ob Entity-Komponente ein Sensor ist""" - return component in MQTTDeviceConverter.SENSOR_COMPONENTS - - @staticmethod - def convert_device_to_actors_and_sensors(device_id: str, device_data: Dict) -> tuple: - """ - Konvertiert ein Gerät mit allen seinen Entities in Actor/Sensor-Format - - Args: - device_id: Geräte-ID (node_id) - device_data: Device-Daten mit Entities-Liste - - Returns: - Tuple (actor_dict or None, sensor_dict or None) - """ - entities = device_data['entities'] - device_info = device_data.get('device_info') - # Gerätename vom ersten Entity oder aus device_info - device_name = device_info.get('name', device_id) - device_mf = device_info.get('model', device_info.get('mdl', device_info.get('mf','MQTT'))) - if(device_info.get('mf','MQTT') == "OpenDTU"): - device_mf = "Wechselrichter" - #if not device_name or device_name == device_id: - # Fallback: Name vom ersten Entity - # if entities: - # device_name = entities[0]['config'].get('name', device_id) - - # Device URL - device_url = f"mqtt://{device_id}" - - # Entities nach Actor/Sensor trennen - actor_entities = [e for e in entities if MQTTDeviceConverter.is_actor_entity(e['component'])] - sensor_entities = [e for e in entities if MQTTDeviceConverter.is_sensor_entity(e['component'])] - - actor_result = None - sensor_result = None - - # Actor erstellen falls Actor-Entities vorhanden - if actor_entities: - commands = [] - states = [] - - for entity in actor_entities: - component = entity['component'] - object_id = entity.get('object_id', 'unknown') - config = entity['config'] - if(device_mf == "MQTT"): - device_mf = config.get('device', device_info.get('dev',{'model': 'MQTT'})).get('model', device_info.get('mdl', device_info.get('mf','MQTT'))) - - # Command aus der Entity erstellen - command_entry = MQTTDeviceConverter._entity_to_commands(component, object_id, config) - if command_entry: - commands.extend(command_entry) - - # States aus der Entity extrahieren - entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) - states.extend(entity_states) - - if commands: # Nur Actor erstellen wenn Commands vorhanden - actor_result = { - 'name': device_name, - 'type': device_mf, # Allgemeiner Typ für Multi-Entity-Geräte - 'url': device_url, - 'commands': commands, - 'states': states - } - - # Sensor erstellen falls Sensor-Entities vorhanden - if sensor_entities: - states = [] - - for entity in sensor_entities: - component = entity['component'] - object_id = entity.get('object_id', 'unknown') - config = entity['config'] - if(device_mf == "MQTT"): - device_mf = device_info.get('model', device_info.get('mdl', device_info.get('mf','MQTT'))) - # States aus der Entity extrahieren - entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) - states.extend(entity_states) - - if states: # Nur Sensor erstellen wenn States vorhanden - sensor_result = { - 'name': device_name, - 'type': device_mf, - 'url': device_url, - 'states': states - } - - return actor_result, sensor_result - - @staticmethod - def _entity_to_commands(component: str, object_id: str, config: Dict) -> Optional[Dict]: - """ - Konvertiert eine MQTT Entity in ein Command - - Args: - component: Entity-Typ (number, button, switch, etc.) - object_id: Object ID (z.B. set_max_ampere_limit) - config: Entity-Konfiguration - - Returns: - Command-Dictionary oder None - """ - # Command Topic - verschiedene mögliche Feldnamen - command_topic = config.get('command_topic') or config.get('cmd_t') or config.get('temperature_command_topic') - base_topic = config.get('~','') - command_topic = command_topic.replace("~",base_topic) - if not command_topic: - return None - - # Command-Name aus object_id ableiten - #command_name = - # Oder aus dem Namen - command_name = config.get('name', object_id.replace('_', ' ').title().replace(' ', '')) - - command_entry = [{ - 'command': command_name, - 'parameters': [], - 'url': command_topic - }] - - # Parameter basierend auf Component-Typ - if component == 'number': - # Number hat einen Wert-Parameter - param = { - 'name': 'value', - 'type': 'number', - 'url': command_topic - } - - # Min/Max aus Config - if 'min' in config: - param['min'] = config['min'] - if 'max' in config: - param['max'] = config['max'] - - # Unit hinzufügen - verschiedene mögliche Feldnamen - unit = ( - config.get('unit_of_measurement') or - config.get('unit_of_meas') or - config.get('unit') or - config.get('u') - ) - if unit: - param['unit'] = unit - - command_entry[0]['parameters'].append(param) - - elif component == 'select': - # Select hat Optionen - param = { - 'name': 'option', - 'type': 'string', - 'url': command_topic - } - - options = config.get('options') or config.get('ops') - if options: - param['values'] = options - - command_entry[0]['parameters'].append(param) - - elif component in ['switch', 'light']: - # Switch/Light haben on/off - param = { - 'name': 'state', - 'type': 'string', - 'url': command_topic, - 'values': [ - config.get('payload_on', config.get('pl_on', 'ON')), - config.get('payload_off', config.get('pl_off', 'OFF')) - ] - } - command_entry[0]['parameters'].append(param) - - # Brightness für Light - brightness_cmd_topic = ( - config.get('brightness_command_topic') or - config.get('bri_cmd_t') - ) - if component == 'light' and brightness_cmd_topic: - command_entry[0]['parameters'].append({ - 'name': 'brightness', - 'type': 'integer', - 'min': 0, - 'max': 255, - 'url': brightness_cmd_topic - }) - - elif component == 'cover': - # Cover hat position - set_pos_topic = ( - config.get('set_position_topic') or - config.get('pos_cmd_t') - ) - if set_pos_topic: - param = { - 'name': 'position', - 'type': 'integer', - 'min': 0, - 'max': 100, - 'url': set_pos_topic - } - command_entry[0]['parameters'].append(param) - else: - # Nur open/close/stop - param = { - 'name': 'action', - 'type': 'string', - 'url': command_topic, - 'values': ['OPEN', 'CLOSE', 'STOP'] - } - command_entry[0]['parameters'].append(param) - - elif component == 'button': - # Button hat normalerweise keinen Parameter, nur das Topic - param = { - 'name': 'press', - 'type': 'trigger', - 'url': command_topic - } - command_entry[0]['parameters'].append(param) - - elif component == 'climate': - # Climate hat Temperatur-Setpoint - temp_cmd_topic = ( - config.get('temperature_command_topic') or - config.get('temp_cmd_t') - ) - if temp_cmd_topic: - param = { - 'name': 'temperature', - 'type': 'number', - 'url': temp_cmd_topic - } - - if 'min_temp' in config: - param['min'] = config['min_temp'] - if 'max_temp' in config: - param['max'] = config['max_temp'] - command_entry[0]['command'] = "Solltemperatur" - command_entry[0]['parameters'].append(param) - - mode_cmd_topic = ( - config.get('mode_command_topic') or - config.get('mode_cmd_t') - ) - if mode_cmd_topic: - param = { - 'name': 'mode', - 'type': 'string', - 'url': mode_cmd_topic, - 'values': config.get('modes', []) - } - command_entry.append({ - 'command': "Modus", - 'parameters': [param] - }) - else: - # Generischer Command mit dem Topic - param = { - 'name': 'value', - 'type': 'string', - 'url': command_topic - } - command_entry['parameters'].append(param) - - return command_entry - - @staticmethod - def _value_path(config: Dict) -> Optional[str]: - """ - Liest aus dem value_template heraus, welcher Teil der Nutzlast gemeint - ist. - - Mehrere Messwerte teilen sich oft ein Topic: der go-eCharger schickt - sechzehn Zahlen als JSON-Feld auf "nrg", und erst das value_template - sagt, dass "Strom L1" das vierte Element ist. Ohne diese Angabe - bekaemen alle sechzehn denselben Rohtext. - - Zurueck kommt ein Pfad in derselben Schreibweise, die auch WLED - benutzt: "[3]", "ssid", "foo[0].bar". None heisst: die ganze Nutzlast. - - Home Assistant erlaubt in value_template beliebiges Jinja. Hier wird - nur der einfache Fall gelesen - ein Zugriff auf value_json und was - danach an Punkten und Klammern folgt. Bei allem anderen (Werttabellen - wie ['Idle','Charging'][value_json|int], Bedingungen) bleibt es beim - Rohtext, also beim bisherigen Verhalten. - """ - vorlage = config.get('value_template') or config.get('val_tpl') - if not vorlage: - return None - treffer = re.search( - r"value_json((?:\s*(?:\.\w+|\[\s*'[^']*'\s*\]|\[\s*\"[^\"]*\"\s*\]|\[\s*\d+\s*\]))*)", - vorlage) - if not treffer or not treffer.group(1).strip(): - return None - teile = [] - for stueck in re.finditer( - r"\.(\w+)|\[\s*'([^']*)'\s*\]|\[\s*\"([^\"]*)\"\s*\]|\[\s*(\d+)\s*\]", - treffer.group(1)): - name = stueck.group(1) or stueck.group(2) or stueck.group(3) - teile.append(name if name else "[%s]" % stueck.group(4)) - if not teile: - return None - # "[3]" haengt sich ohne Punkt an, Namen mit Punkt. - pfad = "" - for teil in teile: - pfad += teil if teil.startswith("[") else (("." + teil) if pfad else teil) - return pfad[:120] - - @staticmethod - def _value_map(config: Dict) -> Optional[List[Dict]]: - """ - Liest eine Werttabelle aus dem value_template. - - Manche Geraete schicken eine Zahl und meinen einen Zustand: - - {{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int ]}} - {{ ['Default','Eco','NextTrip'][value_json|int-3]}} - - Das ist kein Pfad in die Nutzlast, sondern eine Uebersetzung von Zahl - nach Text. Zurueck kommt sie in der Schreibweise, die WLED fuer seine - Effektliste schon benutzt - eine Liste aus {Wert: Bezeichnung} -, - denn die versteht der Editor bereits und macht eine Auswahlliste - daraus. - - Der Versatz im Ausdruck wandert dabei in die Schluessel: bei - "value_json|int-3" gehoert der erste Eintrag zur gesendeten 3. - """ - vorlage = config.get('value_template') or config.get('val_tpl') - if not vorlage: - return None - treffer = re.search( - r"\[\s*((?:'[^']*'|\"[^\"]*\")(?:\s*,\s*(?:'[^']*'|\"[^\"]*\"))*)\s*\]" - r"\s*\[\s*value_json\s*\|\s*int\s*([+-]\s*\d+)?\s*\]", - vorlage) - if not treffer: - return None - bezeichnungen = [t[1:-1] for t in - re.findall(r"'[^']*'|\"[^\"]*\"", treffer.group(1))] - versatz = int(treffer.group(2).replace(' ', '')) if treffer.group(2) else 0 - # index = wert + versatz -> wert = index - versatz - return [{str(i - versatz): name} for i, name in enumerate(bezeichnungen)] - - @staticmethod - def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]: - """ - Extrahiert States aus einer MQTT Entity - - Args: - component: Entity-Typ - object_id: Object ID - config: Entity-Konfiguration - - Returns: - Liste von State-Dictionaries - """ - states = [] - - # State Topic - verschiedene mögliche Feldnamen prüfen - state_topic = ( - config.get('state_topic') or - config.get('stat_t') - ) - - # Bei number/select: oft kein separates state_topic, dann command_topic verwenden - if not state_topic and component in ['number', 'select', 'button']: - # Bei diesen Komponenten kann der State über command_topic abgefragt werden - # oder es gibt ein explizites state_topic - state_topic = config.get('command_topic') or config.get('cmd_t') - - if state_topic: - base_topic = config.get('~','') - state_topic = state_topic.replace("~",base_topic) - state_entry = { - 'name': object_id, - 'type': 'string', - 'url': state_topic, - 'value_path': MQTTDeviceConverter._value_path(config) - } - wertetabelle = MQTTDeviceConverter._value_map(config) - if wertetabelle: - state_entry['values'] = wertetabelle - - # Unit hinzufügen - verschiedene mögliche Feldnamen - unit = ( - config.get('unit_of_measurement') or - config.get('unit_of_meas') or - config.get('unit') or - config.get('u') # Weitere Abkürzung - ) - if unit: - state_entry['unit'] = unit - - # Device Class als zusätzliche Info - if 'device_class' in config: - state_entry['device_class'] = config['device_class'] - elif 'dev_cla' in config: - state_entry['device_class'] = config['dev_cla'] - - # Typ anpassen basierend auf Component - if component == 'number': - state_entry['type'] = 'number' - elif component == 'binary_sensor': - state_entry['type'] = 'boolean' - elif component == 'sensor': - # Bei Sensor den Typ aus value_template ableiten oder number annehmen - state_entry['type'] = 'number' # Default für Sensoren - - # Eine Werttabelle schlaegt den geratenen Typ: was beim Geraet als - # Zahl ankommt, ist fuer den Benutzer ein Zustandsname. - if 'values' in state_entry: - state_entry['type'] = 'string' - - states.append(state_entry) - - # Zusätzliche State Topics (z.B. brightness bei Light) - if component == 'light': - brightness_topic = ( - config.get('brightness_state_topic') or - config.get('bri_stat_t') - ) - if brightness_topic: - states.append({ - 'name': f"{object_id}_brightness", - 'type': 'integer', - 'url': brightness_topic - }) - - if component == 'cover': - position_topic = ( - config.get('position_topic') or - config.get('pos_t') - ) - if position_topic: - states.append({ - 'name': f"{object_id}_position", - 'type': 'integer', - 'url': position_topic - }) - - if component == 'climate': - current_temp_topic = ( - config.get('current_temperature_topic') or - config.get('curr_temp_t') - ) - if current_temp_topic: - states.append({ - 'name': f"{object_id}_current_temp", - 'type': 'number', - 'unit': '°C', - 'url': current_temp_topic - }) - - return states - - -# ============================================================================ -# MODULE WRAPPER -# ============================================================================ - -from modules.base_module import BaseModule - - -class MQTTModule(BaseModule): - """ - MQTT Modul - Implementiert BaseModule Interface - Gibt Actors/Sensors zurück, KEINE DB-Operationen - """ - - def is_enabled(self) -> bool: - """Prüft ob MQTT aktiviert ist""" - return self.config.mqtt_enable - - def discover(self): - """ - Führt MQTT Discovery durch - - Returns: - Tuple (actors, sensors) - """ - logger.info("\n" + "=" * 60) - logger.info("MQTT/HOME ASSISTANT DISCOVERY") - logger.info("=" * 60) - - actors = [] - sensors = [] - - try: - mqtt_discovery = HomeAssistantDiscovery( - broker=self.config.mqtt_broker, - port=self.config.mqtt_port, - username=self.config.mqtt_username, - password=self.config.mqtt_password, - discovery_prefix=self.config.mqtt_discovery_prefix - ) - - if mqtt_discovery.connect(): - mqtt_entities = mqtt_discovery.discover_devices( - timeout=self.config.mqtt_discovery_timeout - ) - - mqtt_devices = MQTTDeviceConverter.group_entities_by_device(mqtt_entities) - - if not mqtt_devices: - logger.info("Keine MQTT-Geräte gefunden") - else: - logger.info(f"{len(mqtt_devices)} MQTT-Geräte gefunden (aus {len(mqtt_entities)} Entities)") - - for device_id, device_data in mqtt_devices.items(): - try: - actor_data, sensor_data = MQTTDeviceConverter.convert_device_to_actors_and_sensors( - device_id, device_data - ) - if actor_data: - actors.append(actor_data) - - if sensor_data: - sensors.append(sensor_data) - - except Exception as e: - logger.error(f"✗ Fehler beim Verarbeiten von MQTT-Gerät {device_id}: {e}") - - mqtt_discovery.disconnect() - else: - logger.error("MQTT-Verbindung fehlgeschlagen") - - except Exception as e: - logger.error(f"✗ MQTT Discovery Fehler: {e}") - - logger.info(f"MQTT: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden") +#!/usr/bin/env python3 +""" +MQTT/Home Assistant Discovery Integration +Erweitert das Tahoma Script um MQTT-Geräte via Home Assistant Discovery +""" + +import paho.mqtt.client as mqtt +import json +import re +import time +from typing import Dict, List, Optional +import logging + +logger = logging.getLogger(__name__) + + +class HomeAssistantDiscovery: + """Klasse für Home Assistant MQTT Discovery""" + + # Bekannte Discovery-Komponenten + COMPONENTS = [ + 'binary_sensor', 'sensor', 'switch', 'light', 'cover', + 'climate', 'fan', 'lock', 'camera', 'vacuum', 'alarm_control_panel', + 'device_tracker', 'number', 'select', 'button', 'text' + ] + + def __init__(self, broker: str, port: int = 1883, username: str = None, + password: str = None, discovery_prefix: str = 'homeassistant'): + """ + Initialisiert Home Assistant Discovery + + Args: + broker: MQTT Broker IP/Hostname + port: MQTT Port (Standard: 1883) + username: MQTT Benutzername (optional) + password: MQTT Passwort (optional) + discovery_prefix: Discovery Prefix (Standard: 'homeassistant') + """ + self.broker = broker + self.port = port + self.username = username + self.password = password + self.discovery_prefix = discovery_prefix + self.client = None + self.discovered_devices = {} + + def connect(self) -> bool: + """ + Verbindet mit dem MQTT Broker + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + self.client = mqtt.Client() + + if self.username and self.password: + self.client.username_pw_set(self.username, self.password) + + self.client.on_connect = self._on_connect + self.client.on_message = self._on_message + + self.client.connect(self.broker, self.port, 60) + logger.info(f"Verbunden mit MQTT Broker {self.broker}:{self.port}") + return True + + except Exception as e: + logger.error(f"MQTT Verbindungsfehler: {e}") + return False + + def _on_connect(self, client, userdata, flags, rc): + """Callback wenn Verbindung hergestellt wurde""" + if rc == 0: + logger.info("MQTT Verbindung erfolgreich") + # Alle Discovery Topics abonnieren mit Wildcard für object_id + for component in self.COMPONENTS: + # Unterstützt beide Topic-Formate: + # homeassistant/component/node_id/config (4 Teile) + # homeassistant/component/node_id/object_id/config (5 Teile) + topic = f"{self.discovery_prefix}/{component}/+/+/config" + client.subscribe(topic) + logger.debug(f"Abonniert: {topic}") + # Zusätzlich auch das kürzere Format abonnieren + topic_short = f"{self.discovery_prefix}/{component}/+/config" + client.subscribe(topic_short) + logger.debug(f"Abonniert: {topic_short}") + else: + logger.error(f"MQTT Verbindung fehlgeschlagen, Code: {rc}") + + def _on_message(self, client, userdata, msg): + """Callback wenn Nachricht empfangen wurde""" + try: + # Topic analysieren - unterstützt beide Formate: + # homeassistant/component/node_id/config (4 Teile) + # homeassistant/component/node_id/object_id/config (5 Teile) + topic_parts = msg.topic.split('/') + + if topic_parts[-1] != 'config': + return # Kein Config-Topic + + if len(topic_parts) == 4: + # Format: homeassistant/component/node_id/config + component = topic_parts[1] + node_id = topic_parts[2] + object_id = None + elif len(topic_parts) == 5: + # Format: homeassistant/component/node_id/object_id/config + component = topic_parts[1] + node_id = topic_parts[2] + object_id = topic_parts[3] + else: + logger.debug(f"Unbekanntes Topic-Format: {msg.topic}") + return + + # Payload parsen + if msg.payload: + config = json.loads(msg.payload.decode('utf-8')) + + # Eindeutigen Key erstellen + if object_id: + device_key = f"{component}_{node_id}_{object_id}" + else: + device_key = f"{component}_{node_id}" + + # Gerät speichern + self.discovered_devices[device_key] = { + 'component': component, + 'node_id': node_id, + 'object_id': object_id, + 'config': config, + 'topic': msg.topic + } + + device_name = config.get('device', config.get('dev',{'name': node_id})).get('name',node_id) + logger.debug(f"Gerät gefunden: {device_name} ({component}) - {msg.topic}") + + except Exception as e: + logger.error(f"Fehler beim Verarbeiten der MQTT-Nachricht von {msg.topic}: {e}") + + def discover_devices(self, timeout: int = 10) -> Dict: + """ + Sucht nach Home Assistant Discovery Geräten + + Args: + timeout: Timeout in Sekunden + + Returns: + Dictionary mit gefundenen Geräten + """ + logger.info(f"Starte Home Assistant Discovery (Timeout: {timeout}s)...") + logger.info(f"Lausche auf {self.discovery_prefix}/+/+/+/config und {self.discovery_prefix}/+/+/config") + + self.discovered_devices = {} + + # MQTT Loop starten + self.client.loop_start() + + # Warten auf Nachrichten - mit Fortschrittsanzeige + for i in range(timeout): + time.sleep(1) + if (i + 1) % 5 == 0 or i == timeout - 1: + logger.info(f"Discovery läuft... {len(self.discovered_devices)} Geräte gefunden ({i+1}/{timeout}s)") + + # Loop stoppen + self.client.loop_stop() + + logger.info(f"✓ {len(self.discovered_devices)} MQTT-Geräte gefunden") + + # Debug: Zeige einige gefundene Topics + if self.discovered_devices: + logger.debug("Gefundene Geräte (Auswahl):") + for i, (key, device) in enumerate(list(self.discovered_devices.items())[:5]): + logger.debug(f" - {device['config'].get('name', key)} ({device['component']}) via {device['topic']}") + if len(self.discovered_devices) > 5: + logger.debug(f" ... und {len(self.discovered_devices) - 5} weitere") + + return self.discovered_devices + + def disconnect(self): + """Trennt die MQTT-Verbindung""" + if self.client: + self.client.disconnect() + logger.info("MQTT-Verbindung getrennt") + + +class MQTTDeviceConverter: + """Konvertiert MQTT Discovery Entities in Datenbank-Format, gruppiert nach Gerät""" + + # Mapping von HA Komponenten zu Actor/Sensor + ACTOR_COMPONENTS = ['switch', 'light', 'cover', 'fan', 'lock', 'climate', + 'vacuum', 'alarm_control_panel', ' ', 'number', 'select'] + SENSOR_COMPONENTS = ['binary_sensor', 'sensor', 'device_tracker'] + + @staticmethod + def group_entities_by_device(discovered_devices: Dict) -> Dict[str, List]: + """ + Gruppiert Discovery-Entities nach Gerät (node_id) + + Args: + discovered_devices: Dictionary mit allen gefundenen Entities + + Returns: + Dictionary: {device_id: [entity1, entity2, ...]} + """ + devices = {} + + for entity_key, entity in discovered_devices.items(): + # Device Identifier aus Config extrahieren + config = entity.get('config', {}) + device_info = config.get('device') or config.get('dev') or {} + # Node ID als Geräte-Identifier verwenden + node_id = entity.get('node_id', 'unknown') + + # Zusätzlich Device Identifiers prüfen falls vorhanden + if device_info and 'identifiers' in device_info: + identifiers = device_info['identifiers'] + if isinstance(identifiers, list) and identifiers: + node_id = identifiers[0] + + if node_id not in devices: + devices[node_id] = { + 'entities': [], + 'device_info': device_info, + 'node_id': node_id + } + + devices[node_id]['entities'].append(entity) + + return devices + + @staticmethod + def is_actor_entity(component: str) -> bool: + """Prüft ob Entity-Komponente ein Aktor ist""" + if(component == "climate"): + logger.info("Climate device gefunden"); + return component in MQTTDeviceConverter.ACTOR_COMPONENTS + + @staticmethod + def is_sensor_entity(component: str) -> bool: + """Prüft ob Entity-Komponente ein Sensor ist""" + return component in MQTTDeviceConverter.SENSOR_COMPONENTS + + @staticmethod + def convert_device_to_actors_and_sensors(device_id: str, device_data: Dict) -> tuple: + """ + Konvertiert ein Gerät mit allen seinen Entities in Actor/Sensor-Format + + Args: + device_id: Geräte-ID (node_id) + device_data: Device-Daten mit Entities-Liste + + Returns: + Tuple (actor_dict or None, sensor_dict or None) + """ + entities = device_data['entities'] + device_info = device_data.get('device_info') + # Gerätename vom ersten Entity oder aus device_info + device_name = device_info.get('name', device_id) + device_mf = device_info.get('model', device_info.get('mdl', device_info.get('mf','MQTT'))) + if(device_info.get('mf','MQTT') == "OpenDTU"): + device_mf = "Wechselrichter" + #if not device_name or device_name == device_id: + # Fallback: Name vom ersten Entity + # if entities: + # device_name = entities[0]['config'].get('name', device_id) + + # Device URL + device_url = f"mqtt://{device_id}" + + # Entities nach Actor/Sensor trennen + actor_entities = [e for e in entities if MQTTDeviceConverter.is_actor_entity(e['component'])] + sensor_entities = [e for e in entities if MQTTDeviceConverter.is_sensor_entity(e['component'])] + + actor_result = None + sensor_result = None + + # Actor erstellen falls Actor-Entities vorhanden + if actor_entities: + commands = [] + states = [] + + for entity in actor_entities: + component = entity['component'] + object_id = entity.get('object_id', 'unknown') + config = entity['config'] + if(device_mf == "MQTT"): + device_mf = config.get('device', device_info.get('dev',{'model': 'MQTT'})).get('model', device_info.get('mdl', device_info.get('mf','MQTT'))) + + # Command aus der Entity erstellen + command_entry = MQTTDeviceConverter._entity_to_commands(component, object_id, config) + if command_entry: + commands.extend(command_entry) + + # States aus der Entity extrahieren + entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) + states.extend(entity_states) + + if commands: # Nur Actor erstellen wenn Commands vorhanden + actor_result = { + 'name': device_name, + 'type': device_mf, # Allgemeiner Typ für Multi-Entity-Geräte + 'url': device_url, + 'commands': commands, + 'states': states + } + + # Sensor erstellen falls Sensor-Entities vorhanden + if sensor_entities: + states = [] + + for entity in sensor_entities: + component = entity['component'] + object_id = entity.get('object_id', 'unknown') + config = entity['config'] + if(device_mf == "MQTT"): + device_mf = device_info.get('model', device_info.get('mdl', device_info.get('mf','MQTT'))) + # States aus der Entity extrahieren + entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) + states.extend(entity_states) + + if states: # Nur Sensor erstellen wenn States vorhanden + sensor_result = { + 'name': device_name, + 'type': device_mf, + 'url': device_url, + 'states': states + } + + return actor_result, sensor_result + + @staticmethod + def _entity_to_commands(component: str, object_id: str, config: Dict) -> Optional[Dict]: + """ + Konvertiert eine MQTT Entity in ein Command + + Args: + component: Entity-Typ (number, button, switch, etc.) + object_id: Object ID (z.B. set_max_ampere_limit) + config: Entity-Konfiguration + + Returns: + Command-Dictionary oder None + """ + # Command Topic - verschiedene mögliche Feldnamen + command_topic = config.get('command_topic') or config.get('cmd_t') or config.get('temperature_command_topic') + base_topic = config.get('~','') + command_topic = command_topic.replace("~",base_topic) + if not command_topic: + return None + + # Command-Name aus object_id ableiten + #command_name = + # Oder aus dem Namen + command_name = config.get('name', object_id.replace('_', ' ').title().replace(' ', '')) + + command_entry = [{ + 'command': command_name, + 'parameters': [], + 'url': command_topic + }] + + # Parameter basierend auf Component-Typ + if component == 'number': + # Number hat einen Wert-Parameter + param = { + 'name': 'value', + 'type': 'number', + 'url': command_topic + } + + # Min/Max aus Config + if 'min' in config: + param['min'] = config['min'] + if 'max' in config: + param['max'] = config['max'] + + # Unit hinzufügen - verschiedene mögliche Feldnamen + unit = ( + config.get('unit_of_measurement') or + config.get('unit_of_meas') or + config.get('unit') or + config.get('u') + ) + if unit: + param['unit'] = unit + + command_entry[0]['parameters'].append(param) + + elif component == 'select': + # Select hat Optionen + param = { + 'name': 'option', + 'type': 'string', + 'url': command_topic + } + + options = config.get('options') or config.get('ops') + if options: + param['values'] = options + + command_entry[0]['parameters'].append(param) + + elif component in ['switch', 'light']: + # Switch/Light haben on/off + param = { + 'name': 'state', + 'type': 'string', + 'url': command_topic, + 'values': [ + config.get('payload_on', config.get('pl_on', 'ON')), + config.get('payload_off', config.get('pl_off', 'OFF')) + ] + } + command_entry[0]['parameters'].append(param) + + # Brightness für Light + brightness_cmd_topic = ( + config.get('brightness_command_topic') or + config.get('bri_cmd_t') + ) + if component == 'light' and brightness_cmd_topic: + command_entry[0]['parameters'].append({ + 'name': 'brightness', + 'type': 'integer', + 'min': 0, + 'max': 255, + 'url': brightness_cmd_topic + }) + + elif component == 'cover': + # Cover hat position + set_pos_topic = ( + config.get('set_position_topic') or + config.get('pos_cmd_t') + ) + if set_pos_topic: + param = { + 'name': 'position', + 'type': 'integer', + 'min': 0, + 'max': 100, + 'url': set_pos_topic + } + command_entry[0]['parameters'].append(param) + else: + # Nur open/close/stop + param = { + 'name': 'action', + 'type': 'string', + 'url': command_topic, + 'values': ['OPEN', 'CLOSE', 'STOP'] + } + command_entry[0]['parameters'].append(param) + + elif component == 'button': + # Button hat normalerweise keinen Parameter, nur das Topic + param = { + 'name': 'press', + 'type': 'trigger', + 'url': command_topic + } + command_entry[0]['parameters'].append(param) + + elif component == 'climate': + # Climate hat Temperatur-Setpoint + temp_cmd_topic = ( + config.get('temperature_command_topic') or + config.get('temp_cmd_t') + ) + if temp_cmd_topic: + param = { + 'name': 'temperature', + 'type': 'number', + 'url': temp_cmd_topic + } + + if 'min_temp' in config: + param['min'] = config['min_temp'] + if 'max_temp' in config: + param['max'] = config['max_temp'] + command_entry[0]['command'] = "Solltemperatur" + command_entry[0]['parameters'].append(param) + + mode_cmd_topic = ( + config.get('mode_command_topic') or + config.get('mode_cmd_t') + ) + if mode_cmd_topic: + param = { + 'name': 'mode', + 'type': 'string', + 'url': mode_cmd_topic, + 'values': config.get('modes', []) + } + command_entry.append({ + 'command': "Modus", + 'parameters': [param] + }) + else: + # Generischer Command mit dem Topic + param = { + 'name': 'value', + 'type': 'string', + 'url': command_topic + } + command_entry['parameters'].append(param) + + return command_entry + + @staticmethod + def _value_path(config: Dict) -> Optional[str]: + """ + Liest aus dem value_template heraus, welcher Teil der Nutzlast gemeint + ist. + + Mehrere Messwerte teilen sich oft ein Topic: der go-eCharger schickt + sechzehn Zahlen als JSON-Feld auf "nrg", und erst das value_template + sagt, dass "Strom L1" das vierte Element ist. Ohne diese Angabe + bekaemen alle sechzehn denselben Rohtext. + + Zurueck kommt ein Pfad in derselben Schreibweise, die auch WLED + benutzt: "[3]", "ssid", "foo[0].bar". None heisst: die ganze Nutzlast. + + Home Assistant erlaubt in value_template beliebiges Jinja. Hier wird + nur der einfache Fall gelesen - ein Zugriff auf value_json und was + danach an Punkten und Klammern folgt. Bei allem anderen (Werttabellen + wie ['Idle','Charging'][value_json|int], Bedingungen) bleibt es beim + Rohtext, also beim bisherigen Verhalten. + """ + vorlage = config.get('value_template') or config.get('val_tpl') + if not vorlage: + return None + treffer = re.search( + r"value_json((?:\s*(?:\.\w+|\[\s*'[^']*'\s*\]|\[\s*\"[^\"]*\"\s*\]|\[\s*\d+\s*\]))*)", + vorlage) + if not treffer or not treffer.group(1).strip(): + return None + teile = [] + for stueck in re.finditer( + r"\.(\w+)|\[\s*'([^']*)'\s*\]|\[\s*\"([^\"]*)\"\s*\]|\[\s*(\d+)\s*\]", + treffer.group(1)): + name = stueck.group(1) or stueck.group(2) or stueck.group(3) + teile.append(name if name else "[%s]" % stueck.group(4)) + if not teile: + return None + # "[3]" haengt sich ohne Punkt an, Namen mit Punkt. + pfad = "" + for teil in teile: + pfad += teil if teil.startswith("[") else (("." + teil) if pfad else teil) + return pfad[:120] + + @staticmethod + def _value_map(config: Dict) -> Optional[List[Dict]]: + """ + Liest eine Werttabelle aus dem value_template. + + Manche Geraete schicken eine Zahl und meinen einen Zustand: + + {{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int ]}} + {{ ['Default','Eco','NextTrip'][value_json|int-3]}} + + Das ist kein Pfad in die Nutzlast, sondern eine Uebersetzung von Zahl + nach Text. Zurueck kommt sie in der Schreibweise, die WLED fuer seine + Effektliste schon benutzt - eine Liste aus {Wert: Bezeichnung} -, + denn die versteht der Editor bereits und macht eine Auswahlliste + daraus. + + Der Versatz im Ausdruck wandert dabei in die Schluessel: bei + "value_json|int-3" gehoert der erste Eintrag zur gesendeten 3. + """ + vorlage = config.get('value_template') or config.get('val_tpl') + if not vorlage: + return None + treffer = re.search( + r"\[\s*((?:'[^']*'|\"[^\"]*\")(?:\s*,\s*(?:'[^']*'|\"[^\"]*\"))*)\s*\]" + r"\s*\[\s*value_json\s*\|\s*int\s*([+-]\s*\d+)?\s*\]", + vorlage) + if not treffer: + return None + bezeichnungen = [t[1:-1] for t in + re.findall(r"'[^']*'|\"[^\"]*\"", treffer.group(1))] + versatz = int(treffer.group(2).replace(' ', '')) if treffer.group(2) else 0 + # index = wert + versatz -> wert = index - versatz + return [{str(i - versatz): name} for i, name in enumerate(bezeichnungen)] + + @staticmethod + def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]: + """ + Extrahiert States aus einer MQTT Entity + + Args: + component: Entity-Typ + object_id: Object ID + config: Entity-Konfiguration + + Returns: + Liste von State-Dictionaries + """ + states = [] + + # State Topic - verschiedene mögliche Feldnamen prüfen + state_topic = ( + config.get('state_topic') or + config.get('stat_t') + ) + + # Bei number/select: oft kein separates state_topic, dann command_topic verwenden + if not state_topic and component in ['number', 'select', 'button']: + # Bei diesen Komponenten kann der State über command_topic abgefragt werden + # oder es gibt ein explizites state_topic + state_topic = config.get('command_topic') or config.get('cmd_t') + + if state_topic: + base_topic = config.get('~','') + state_topic = state_topic.replace("~",base_topic) + state_entry = { + 'name': object_id, + 'type': 'string', + 'url': state_topic, + 'value_path': MQTTDeviceConverter._value_path(config) + } + wertetabelle = MQTTDeviceConverter._value_map(config) + if wertetabelle: + state_entry['values'] = wertetabelle + + # Unit hinzufügen - verschiedene mögliche Feldnamen + unit = ( + config.get('unit_of_measurement') or + config.get('unit_of_meas') or + config.get('unit') or + config.get('u') # Weitere Abkürzung + ) + if unit: + state_entry['unit'] = unit + + # Device Class als zusätzliche Info + if 'device_class' in config: + state_entry['device_class'] = config['device_class'] + elif 'dev_cla' in config: + state_entry['device_class'] = config['dev_cla'] + + # Typ anpassen basierend auf Component + if component == 'number': + state_entry['type'] = 'number' + elif component == 'binary_sensor': + state_entry['type'] = 'boolean' + elif component == 'sensor': + # Bei Sensor den Typ aus value_template ableiten oder number annehmen + state_entry['type'] = 'number' # Default für Sensoren + + # Eine Werttabelle schlaegt den geratenen Typ: was beim Geraet als + # Zahl ankommt, ist fuer den Benutzer ein Zustandsname. + if 'values' in state_entry: + state_entry['type'] = 'string' + + states.append(state_entry) + + # Zusätzliche State Topics (z.B. brightness bei Light) + if component == 'light': + brightness_topic = ( + config.get('brightness_state_topic') or + config.get('bri_stat_t') + ) + if brightness_topic: + states.append({ + 'name': f"{object_id}_brightness", + 'type': 'integer', + 'url': brightness_topic + }) + + if component == 'cover': + position_topic = ( + config.get('position_topic') or + config.get('pos_t') + ) + if position_topic: + states.append({ + 'name': f"{object_id}_position", + 'type': 'integer', + 'url': position_topic + }) + + if component == 'climate': + current_temp_topic = ( + config.get('current_temperature_topic') or + config.get('curr_temp_t') + ) + if current_temp_topic: + states.append({ + 'name': f"{object_id}_current_temp", + 'type': 'number', + 'unit': '°C', + 'url': current_temp_topic + }) + + return states + + +# ============================================================================ +# MODULE WRAPPER +# ============================================================================ + +from modules.base_module import BaseModule + + +class MQTTModule(BaseModule): + """ + MQTT Modul - Implementiert BaseModule Interface + Gibt Actors/Sensors zurück, KEINE DB-Operationen + """ + + def is_enabled(self) -> bool: + """Prüft ob MQTT aktiviert ist""" + return self.config.mqtt_enable + + def discover(self): + """ + Führt MQTT Discovery durch + + Returns: + Tuple (actors, sensors) + """ + logger.info("\n" + "=" * 60) + logger.info("MQTT/HOME ASSISTANT DISCOVERY") + logger.info("=" * 60) + + actors = [] + sensors = [] + + try: + mqtt_discovery = HomeAssistantDiscovery( + broker=self.config.mqtt_broker, + port=self.config.mqtt_port, + username=self.config.mqtt_username, + password=self.config.mqtt_password, + discovery_prefix=self.config.mqtt_discovery_prefix + ) + + if mqtt_discovery.connect(): + mqtt_entities = mqtt_discovery.discover_devices( + timeout=self.config.mqtt_discovery_timeout + ) + + mqtt_devices = MQTTDeviceConverter.group_entities_by_device(mqtt_entities) + + if not mqtt_devices: + logger.info("Keine MQTT-Geräte gefunden") + else: + logger.info(f"{len(mqtt_devices)} MQTT-Geräte gefunden (aus {len(mqtt_entities)} Entities)") + + for device_id, device_data in mqtt_devices.items(): + try: + actor_data, sensor_data = MQTTDeviceConverter.convert_device_to_actors_and_sensors( + device_id, device_data + ) + if actor_data: + actors.append(actor_data) + + if sensor_data: + sensors.append(sensor_data) + + except Exception as e: + logger.error(f"✗ Fehler beim Verarbeiten von MQTT-Gerät {device_id}: {e}") + + mqtt_discovery.disconnect() + else: + logger.error("MQTT-Verbindung fehlgeschlagen") + + except Exception as e: + logger.error(f"✗ MQTT Discovery Fehler: {e}") + + logger.info(f"MQTT: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden") return actors, sensors \ No newline at end of file diff --git a/restricted/deviceDiscovery/modules/shelly_module.py b/restricted/deviceDiscovery/modules/shelly_module.py index cef8b2c..864ee77 100644 --- a/restricted/deviceDiscovery/modules/shelly_module.py +++ b/restricted/deviceDiscovery/modules/shelly_module.py @@ -1,479 +1,479 @@ - - - -#!/usr/bin/env python3 -""" -Shelly Module -Enthält NUR Shelly-spezifische Geräte-Discovery Logik -KEINE Datenbank-Operationen! -""" - -import json -import requests -import socket -import concurrent.futures -import logging -from typing import List, Dict, Optional, Tuple -from modules.base_module import BaseModule - -# Logging konfigurieren -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -class ShellyDiscovery: - """Klasse zum Entdecken von Shelly-Geräten im Netzwerk""" - - SHELLY_MDNS_SERVICE = "_http._tcp.local." - COMMON_PORTS = [80] - - def __init__(self): - self.devices = [] - - - def scan_network(self, network: str = None, max_threads: int = 50) -> List[str]: - """Scannt das Netzwerk nach Shelly-Geräten""" - if network is None: - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - local_ip = s.getsockname()[0] - s.close() - network_prefix = '.'.join(local_ip.split('.')[:-1]) - except: - logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x") - network_prefix = "192.168.1" - else: - network_prefix = '.'.join(network.split('.')[:3]) - - logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach Shelly-Geräten...") - - def check_ip(ip): - device = ShellyDiscovery.is_shelly_device(ip) - if device: - return device - return None - - shelly_devices = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor: - futures = [executor.submit(check_ip, f"{network_prefix}.{i}") - for i in range(1, 255)] - - for future in concurrent.futures.as_completed(futures): - result = future.result() - if result: - shelly_devices.append(result) - #logger.info(f"Shelly-Gerät gefunden: {result}") - - return shelly_devices - """def scan_network(self, start_ip: int = 2, end_ip: int = 254, timeout: float = 0.3) -> List[str]: - - active_hosts = [] - logger.info(f"Scanne Netzwerk {self.network_range}.{start_ip}-{end_ip}...") - - for i in range(start_ip, end_ip + 1): - ip = f"{self.network_range}.{i}" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - - try: - result = sock.connect_ex((ip, 80)) - if result == 0: - active_hosts.append(ip) - logger.debug(f"Host gefunden: {ip}") - except: - pass - finally: - sock.close() - - logger.info(f"{len(active_hosts)} aktive Hosts gefunden") - return active_hosts - """ - def is_shelly_device(ip: str) -> Optional[Dict]: - """ - Prüft ob ein Host ein Shelly-Gerät ist - - Args: - ip: IP-Adresse des Hosts - - Returns: - Device Info Dict wenn Shelly, sonst None - """ - #logger.info(f"Suche Shelly Getät unter: {ip}") - try: - # Versuche Gen2 API (neuere Shelly-Geräte) - response = requests.get( - f"http://{ip}/rpc/Shelly.GetDeviceInfo", - timeout=2 - ) - if response.status_code == 200: - data = response.json() - logger.info(f"Shelly Gen2 Gerät gefunden: {ip} - {data.get('name', 'Unknown')}") - return { - 'ip': ip, - 'generation': 2, - 'info': data - } - except: - pass - - try: - # Versuche Gen1 API (ältere Shelly-Geräte) - response = requests.get( - f"http://{ip}/settings", - timeout=2 - ) - if response.status_code == 200: - data = response.json() - if 'device' in data and (data['device']['type'].startswith('SHELLY') or data['device']['type'].startswith('SHSW')): - logger.info(f"Shelly Gen1 Gerät gefunden: {ip} - {data['device'].get('type', 'Unknown')}") - return { - 'ip': ip, - 'generation': 1, - 'info': data - } - except: - pass - - return None - - def get_device_status(self, device: Dict) -> Optional[Dict]: - """ - Holt den Status eines Shelly-Geräts - - Args: - device: Device Info Dictionary - - Returns: - Status Dictionary oder None - """ - ip = device['ip'] - - try: - if device['generation'] == 2: - # Gen2 Status - response = requests.get( - f"http://{ip}/rpc/Shelly.GetStatus", - timeout=2 - ) - if response.status_code == 200: - return response.json() - else: - # Gen1 Status - response = requests.get( - f"http://{ip}/status", - timeout=2 - ) - if response.status_code == 200: - return response.json() - except Exception as e: - logger.error(f"Fehler beim Abrufen des Status von {ip}: {e}") - - return None - - def discover_devices(self) -> List[Dict]: - """ - Entdeckt alle Shelly-Geräte im Netzwerk - - Returns: - Liste von Shelly-Geräten mit Status - """ - self.devices = self.scan_network() - - for idx, device in enumerate(self.devices): - status = self.get_device_status(device) - self.devices[idx]['status'] = status - - logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt") - return self.devices - - - -# ============================================================================ -# MODULE WRAPPER -# ============================================================================ - -class ShellyModule(BaseModule): - """ - Shelly Modul - Implementiert BaseModule Interface - Gibt Actors/Sensors zurück, KEINE DB-Operationen - """ - - def is_enabled(self) -> bool: - """Prüft ob Shelly aktiviert ist""" - return self.config.shelly_enable - - def discover(self) -> Tuple[List[Dict], List[Dict]]: - """ - Führt Shelly Discovery durch - - Returns: - Tuple (actors, sensors) - """ - logger.info("\n" + "=" * 60) - logger.info("SHELLY-GERÄTE WERDEN GESUCHT") - logger.info("=" * 60) - - actors = [] - sensors = [] - - try: - discovery = ShellyDiscovery() - devices = discovery.discover_devices() - - if not devices: - logger.info("Keine Shelly-Geräte gefunden") - return actors, sensors - - logger.info(f"{len(devices)} Shelly-Geräte gefunden") - - # Verarbeite jedes Gerät - for device in devices: - if device['generation'] == 2: - device_actors, device_sensors = self._parse_gen2_device(device) - else: - device_actors, device_sensors = self._parse_gen1_device(device) - - actors.extend(device_actors) - sensors.extend(device_sensors) - - except Exception as e: - logger.error(f"✗ Shelly Discovery Fehler: {e}") - - logger.info(f"Shelly: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden") - return actors, sensors - - def _parse_gen2_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]: - """Parst Gen2 Shelly-Gerät""" - actors = [] - sensors = [] - - info = device.get('info', {}) - status = device.get('status', {}) - ip = device['ip'] - - device_name = info.get('name', f"Shelly_{info.get('id', ip)}") - device_model = info.get('model', 'Unknown') - - # Switches als Aktoren - switch_count = sum(1 for key in status.keys() if key.startswith('switch:')) - for i in range(switch_count): - switch_data = status.get(f'switch:{i}', {}) - - actors.append({ - 'type': f'ShellySwitch_{device_model}'.replace(' ', '_'), - 'name': f"{device_name}_Switch_{i}", - 'url': f"http://{ip}/rpc/Switch.Set?id={i}", - 'commands': [ - {'command': 'turn_on', 'url': 'on=true', 'parameters': []}, - {'command': 'turn_off', 'url': 'on=false', 'parameters': []}, - {'command': 'toggle', 'parameters': []} - ], - 'states': [ - { - 'name': 'output', - 'type': 'boolean', - 'url': 'output', - 'current_value': switch_data.get('output', False) - } - ] - }) - - # Temperatursensoren - temp_count = sum(1 for key in status.keys() if key.startswith('temperature:')) - for i in range(temp_count): - temp_data = status.get(f'temperature:{i}', {}) - - sensors.append({ - 'type': 'Temperatur', - 'name': f"{device_name}_Temp_{i}", - 'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}", - 'states': [ - { - 'name': 'temperature', - 'type': 'number', - 'current_value': temp_data.get('tC'), - 'url': f"tC", - 'unit': '°C' - } - ] - }) - # Energy-Meter - em_count = sum(1 for key in status.keys() if key.startswith('em:')) - for i in range(em_count): - em_data = status.get(f'em:{i}', {}) - sensors.append({ - 'type': 'Stromzähler', - 'name': f"{device_name}_EM_{i}", - 'url': f"http://{ip}/rpc/em.GetStatus?id={i}", - 'states': [] - }) - if(em_data.get('a_voltage') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Spannung Phase A', - 'type': 'number', - 'url': 'a_voltage', - 'current_value': em_data.get('a_voltage'), - 'unit': 'V'}) - if(em_data.get('b_voltage') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Spannung Phase B', - 'type': 'number', - 'url': 'b_voltage', - 'current_value': em_data.get('b_voltage'), - 'unit': 'V'}) - if(em_data.get('c_voltage') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Spannung Phase C', - 'type': 'number', - 'url': 'c_voltage', - 'current_value': em_data.get('c_voltage'), - 'unit': 'V'}) - if(em_data.get('a_current') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Strom Phase A', - 'type': 'number', - 'url': 'a_current', - 'current_value': em_data.get('a_current'), - 'unit': 'A'}) - if(em_data.get('b_current') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Strom Phase B', - 'type': 'number', - 'url': 'b_current', - 'current_value': em_data.get('b_current'), - 'unit': 'A'}) - if(em_data.get('c_current') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Strom Phase C', - 'type': 'number', - 'url': 'c_current', - 'current_value': em_data.get('c_current'), - 'unit': 'A'}) - if(em_data.get('a_act_power') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Wirkleistung Phase A', - 'type': 'number', - 'url': 'a_act_power', - 'current_value': em_data.get('a_act_power'), - 'unit': 'W'}) - if(em_data.get('b_act_power') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Wirkleistung Phase B', - 'type': 'number', - 'url': 'b_act_power', - 'current_value': em_data.get('b_act_power'), - 'unit': 'W'}) - if(em_data.get('c_act_power') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Wirkleistung Phase C', - 'type': 'number', - 'url': 'c_act_power', - 'current_value': em_data.get('c_act_power'), - 'unit': 'W'}) - if(em_data.get('a_aprt_power') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Scheinleistung Phase A', - 'type': 'number', - 'url': 'a_aprt_power', - 'current_value': em_data.get('a_aprt_power'), - 'unit': 'VA'}) - if(em_data.get('b_aprt_power') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Scheinleistung Phase B', - 'type': 'number', - 'url': 'b_aprt_power', - 'current_value': em_data.get('b_aprt_power'), - 'unit': 'VA'}) - if(em_data.get('c_aprt_power') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Scheinleistung Phase C', - 'type': 'number', - 'url': 'c_aprt_power', - 'current_value': em_data.get('c_aprt_power'), - 'unit': 'VA'}) - if(em_data.get('a_freq') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Frequenz Phase A', - 'type': 'number', - 'url': 'a_freq', - 'current_value': em_data.get('a_freq'), - 'unit': 'Hz'}) - if(em_data.get('b_freq') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Frequenz Phase B', - 'type': 'number', - 'url': 'b_freq', - 'current_value': em_data.get('b_freq'), - 'unit': 'Hz'}) - if(em_data.get('c_freq') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Frequenz Phase C', - 'type': 'number', - 'url': 'c_freq', - 'current_value': em_data.get('c_freq'), - 'unit': 'Hz'}) - if(em_data.get('total_act_power') is not None): - sensors[len(sensors)-1]['states'].append({ - 'name': 'Wirkleistung gesamt', - 'type': 'number', - 'url': 'total_act_power', - 'current_value': em_data.get('total_act_power'), - 'unit': 'W'}) - - return actors, sensors - - def _parse_gen1_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]: - """Parst Gen1 Shelly-Gerät""" - actors = [] - sensors = [] - - info = device.get('info', {}) - status = device.get('status', {}) - ip = device['ip'] - - device_name = info.get('name', f"Shelly_{info.get('type', ip)}") - device_type = info['device'].get('type', 'Unknown') - - # Relays als Aktoren - relays = status.get('relays', []) - for i, relay in enumerate(relays): - actors.append({ - 'type': f'ShellyRelay_{device_type}'.replace(' ', '_'), - 'name': f"{device_name}_Relay_{i}", - 'url': f"http://{ip}/relay/{i}", - 'commands': [ - {'command': 'turn_on', 'url': 'turn=on', 'parameters': []}, - {'command': 'turn_off', 'url': 'turn=off', 'parameters': []}, - {'command': 'toggle', 'url': 'turn=toggle', 'parameters': []} - ], - 'states': [ - { - 'name': 'ison', - 'type': 'boolean', - 'url': 'ison', - 'current_value': relay.get('ison', False) - } - ] - }) - - # Temperatursensoren - temp_data = status.get('tmp', {}) - if temp_data and 'tC' in temp_data: - sensors.append({ - 'type': 'Temperatur', - 'name': f"{device_name}_Temp", - 'url': f"http://{ip}/status", - 'states': [ - { - 'name': 'temperature', - 'type': 'number', - 'current_value': temp_data.get('tC'), - 'unit': '°C' - } - ] - }) - + + + +#!/usr/bin/env python3 +""" +Shelly Module +Enthält NUR Shelly-spezifische Geräte-Discovery Logik +KEINE Datenbank-Operationen! +""" + +import json +import requests +import socket +import concurrent.futures +import logging +from typing import List, Dict, Optional, Tuple +from modules.base_module import BaseModule + +# Logging konfigurieren +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class ShellyDiscovery: + """Klasse zum Entdecken von Shelly-Geräten im Netzwerk""" + + SHELLY_MDNS_SERVICE = "_http._tcp.local." + COMMON_PORTS = [80] + + def __init__(self): + self.devices = [] + + + def scan_network(self, network: str = None, max_threads: int = 50) -> List[str]: + """Scannt das Netzwerk nach Shelly-Geräten""" + if network is None: + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + s.close() + network_prefix = '.'.join(local_ip.split('.')[:-1]) + except: + logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x") + network_prefix = "192.168.1" + else: + network_prefix = '.'.join(network.split('.')[:3]) + + logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach Shelly-Geräten...") + + def check_ip(ip): + device = ShellyDiscovery.is_shelly_device(ip) + if device: + return device + return None + + shelly_devices = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor: + futures = [executor.submit(check_ip, f"{network_prefix}.{i}") + for i in range(1, 255)] + + for future in concurrent.futures.as_completed(futures): + result = future.result() + if result: + shelly_devices.append(result) + #logger.info(f"Shelly-Gerät gefunden: {result}") + + return shelly_devices + """def scan_network(self, start_ip: int = 2, end_ip: int = 254, timeout: float = 0.3) -> List[str]: + + active_hosts = [] + logger.info(f"Scanne Netzwerk {self.network_range}.{start_ip}-{end_ip}...") + + for i in range(start_ip, end_ip + 1): + ip = f"{self.network_range}.{i}" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + + try: + result = sock.connect_ex((ip, 80)) + if result == 0: + active_hosts.append(ip) + logger.debug(f"Host gefunden: {ip}") + except: + pass + finally: + sock.close() + + logger.info(f"{len(active_hosts)} aktive Hosts gefunden") + return active_hosts + """ + def is_shelly_device(ip: str) -> Optional[Dict]: + """ + Prüft ob ein Host ein Shelly-Gerät ist + + Args: + ip: IP-Adresse des Hosts + + Returns: + Device Info Dict wenn Shelly, sonst None + """ + #logger.info(f"Suche Shelly Getät unter: {ip}") + try: + # Versuche Gen2 API (neuere Shelly-Geräte) + response = requests.get( + f"http://{ip}/rpc/Shelly.GetDeviceInfo", + timeout=2 + ) + if response.status_code == 200: + data = response.json() + logger.info(f"Shelly Gen2 Gerät gefunden: {ip} - {data.get('name', 'Unknown')}") + return { + 'ip': ip, + 'generation': 2, + 'info': data + } + except: + pass + + try: + # Versuche Gen1 API (ältere Shelly-Geräte) + response = requests.get( + f"http://{ip}/settings", + timeout=2 + ) + if response.status_code == 200: + data = response.json() + if 'device' in data and (data['device']['type'].startswith('SHELLY') or data['device']['type'].startswith('SHSW')): + logger.info(f"Shelly Gen1 Gerät gefunden: {ip} - {data['device'].get('type', 'Unknown')}") + return { + 'ip': ip, + 'generation': 1, + 'info': data + } + except: + pass + + return None + + def get_device_status(self, device: Dict) -> Optional[Dict]: + """ + Holt den Status eines Shelly-Geräts + + Args: + device: Device Info Dictionary + + Returns: + Status Dictionary oder None + """ + ip = device['ip'] + + try: + if device['generation'] == 2: + # Gen2 Status + response = requests.get( + f"http://{ip}/rpc/Shelly.GetStatus", + timeout=2 + ) + if response.status_code == 200: + return response.json() + else: + # Gen1 Status + response = requests.get( + f"http://{ip}/status", + timeout=2 + ) + if response.status_code == 200: + return response.json() + except Exception as e: + logger.error(f"Fehler beim Abrufen des Status von {ip}: {e}") + + return None + + def discover_devices(self) -> List[Dict]: + """ + Entdeckt alle Shelly-Geräte im Netzwerk + + Returns: + Liste von Shelly-Geräten mit Status + """ + self.devices = self.scan_network() + + for idx, device in enumerate(self.devices): + status = self.get_device_status(device) + self.devices[idx]['status'] = status + + logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt") + return self.devices + + + +# ============================================================================ +# MODULE WRAPPER +# ============================================================================ + +class ShellyModule(BaseModule): + """ + Shelly Modul - Implementiert BaseModule Interface + Gibt Actors/Sensors zurück, KEINE DB-Operationen + """ + + def is_enabled(self) -> bool: + """Prüft ob Shelly aktiviert ist""" + return self.config.shelly_enable + + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Führt Shelly Discovery durch + + Returns: + Tuple (actors, sensors) + """ + logger.info("\n" + "=" * 60) + logger.info("SHELLY-GERÄTE WERDEN GESUCHT") + logger.info("=" * 60) + + actors = [] + sensors = [] + + try: + discovery = ShellyDiscovery() + devices = discovery.discover_devices() + + if not devices: + logger.info("Keine Shelly-Geräte gefunden") + return actors, sensors + + logger.info(f"{len(devices)} Shelly-Geräte gefunden") + + # Verarbeite jedes Gerät + for device in devices: + if device['generation'] == 2: + device_actors, device_sensors = self._parse_gen2_device(device) + else: + device_actors, device_sensors = self._parse_gen1_device(device) + + actors.extend(device_actors) + sensors.extend(device_sensors) + + except Exception as e: + logger.error(f"✗ Shelly Discovery Fehler: {e}") + + logger.info(f"Shelly: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden") + return actors, sensors + + def _parse_gen2_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]: + """Parst Gen2 Shelly-Gerät""" + actors = [] + sensors = [] + + info = device.get('info', {}) + status = device.get('status', {}) + ip = device['ip'] + + device_name = info.get('name', f"Shelly_{info.get('id', ip)}") + device_model = info.get('model', 'Unknown') + + # Switches als Aktoren + switch_count = sum(1 for key in status.keys() if key.startswith('switch:')) + for i in range(switch_count): + switch_data = status.get(f'switch:{i}', {}) + + actors.append({ + 'type': f'ShellySwitch_{device_model}'.replace(' ', '_'), + 'name': f"{device_name}_Switch_{i}", + 'url': f"http://{ip}/rpc/Switch.Set?id={i}", + 'commands': [ + {'command': 'turn_on', 'url': 'on=true', 'parameters': []}, + {'command': 'turn_off', 'url': 'on=false', 'parameters': []}, + {'command': 'toggle', 'parameters': []} + ], + 'states': [ + { + 'name': 'output', + 'type': 'boolean', + 'url': 'output', + 'current_value': switch_data.get('output', False) + } + ] + }) + + # Temperatursensoren + temp_count = sum(1 for key in status.keys() if key.startswith('temperature:')) + for i in range(temp_count): + temp_data = status.get(f'temperature:{i}', {}) + + sensors.append({ + 'type': 'Temperatur', + 'name': f"{device_name}_Temp_{i}", + 'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}", + 'states': [ + { + 'name': 'temperature', + 'type': 'number', + 'current_value': temp_data.get('tC'), + 'url': f"tC", + 'unit': '°C' + } + ] + }) + # Energy-Meter + em_count = sum(1 for key in status.keys() if key.startswith('em:')) + for i in range(em_count): + em_data = status.get(f'em:{i}', {}) + sensors.append({ + 'type': 'Stromzähler', + 'name': f"{device_name}_EM_{i}", + 'url': f"http://{ip}/rpc/em.GetStatus?id={i}", + 'states': [] + }) + if(em_data.get('a_voltage') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Spannung Phase A', + 'type': 'number', + 'url': 'a_voltage', + 'current_value': em_data.get('a_voltage'), + 'unit': 'V'}) + if(em_data.get('b_voltage') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Spannung Phase B', + 'type': 'number', + 'url': 'b_voltage', + 'current_value': em_data.get('b_voltage'), + 'unit': 'V'}) + if(em_data.get('c_voltage') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Spannung Phase C', + 'type': 'number', + 'url': 'c_voltage', + 'current_value': em_data.get('c_voltage'), + 'unit': 'V'}) + if(em_data.get('a_current') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Strom Phase A', + 'type': 'number', + 'url': 'a_current', + 'current_value': em_data.get('a_current'), + 'unit': 'A'}) + if(em_data.get('b_current') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Strom Phase B', + 'type': 'number', + 'url': 'b_current', + 'current_value': em_data.get('b_current'), + 'unit': 'A'}) + if(em_data.get('c_current') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Strom Phase C', + 'type': 'number', + 'url': 'c_current', + 'current_value': em_data.get('c_current'), + 'unit': 'A'}) + if(em_data.get('a_act_power') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Wirkleistung Phase A', + 'type': 'number', + 'url': 'a_act_power', + 'current_value': em_data.get('a_act_power'), + 'unit': 'W'}) + if(em_data.get('b_act_power') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Wirkleistung Phase B', + 'type': 'number', + 'url': 'b_act_power', + 'current_value': em_data.get('b_act_power'), + 'unit': 'W'}) + if(em_data.get('c_act_power') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Wirkleistung Phase C', + 'type': 'number', + 'url': 'c_act_power', + 'current_value': em_data.get('c_act_power'), + 'unit': 'W'}) + if(em_data.get('a_aprt_power') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Scheinleistung Phase A', + 'type': 'number', + 'url': 'a_aprt_power', + 'current_value': em_data.get('a_aprt_power'), + 'unit': 'VA'}) + if(em_data.get('b_aprt_power') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Scheinleistung Phase B', + 'type': 'number', + 'url': 'b_aprt_power', + 'current_value': em_data.get('b_aprt_power'), + 'unit': 'VA'}) + if(em_data.get('c_aprt_power') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Scheinleistung Phase C', + 'type': 'number', + 'url': 'c_aprt_power', + 'current_value': em_data.get('c_aprt_power'), + 'unit': 'VA'}) + if(em_data.get('a_freq') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Frequenz Phase A', + 'type': 'number', + 'url': 'a_freq', + 'current_value': em_data.get('a_freq'), + 'unit': 'Hz'}) + if(em_data.get('b_freq') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Frequenz Phase B', + 'type': 'number', + 'url': 'b_freq', + 'current_value': em_data.get('b_freq'), + 'unit': 'Hz'}) + if(em_data.get('c_freq') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Frequenz Phase C', + 'type': 'number', + 'url': 'c_freq', + 'current_value': em_data.get('c_freq'), + 'unit': 'Hz'}) + if(em_data.get('total_act_power') is not None): + sensors[len(sensors)-1]['states'].append({ + 'name': 'Wirkleistung gesamt', + 'type': 'number', + 'url': 'total_act_power', + 'current_value': em_data.get('total_act_power'), + 'unit': 'W'}) + + return actors, sensors + + def _parse_gen1_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]: + """Parst Gen1 Shelly-Gerät""" + actors = [] + sensors = [] + + info = device.get('info', {}) + status = device.get('status', {}) + ip = device['ip'] + + device_name = info.get('name', f"Shelly_{info.get('type', ip)}") + device_type = info['device'].get('type', 'Unknown') + + # Relays als Aktoren + relays = status.get('relays', []) + for i, relay in enumerate(relays): + actors.append({ + 'type': f'ShellyRelay_{device_type}'.replace(' ', '_'), + 'name': f"{device_name}_Relay_{i}", + 'url': f"http://{ip}/relay/{i}", + 'commands': [ + {'command': 'turn_on', 'url': 'turn=on', 'parameters': []}, + {'command': 'turn_off', 'url': 'turn=off', 'parameters': []}, + {'command': 'toggle', 'url': 'turn=toggle', 'parameters': []} + ], + 'states': [ + { + 'name': 'ison', + 'type': 'boolean', + 'url': 'ison', + 'current_value': relay.get('ison', False) + } + ] + }) + + # Temperatursensoren + temp_data = status.get('tmp', {}) + if temp_data and 'tC' in temp_data: + sensors.append({ + 'type': 'Temperatur', + 'name': f"{device_name}_Temp", + 'url': f"http://{ip}/status", + 'states': [ + { + 'name': 'temperature', + 'type': 'number', + 'current_value': temp_data.get('tC'), + 'unit': '°C' + } + ] + }) + return actors, sensors \ No newline at end of file diff --git a/restricted/deviceDiscovery/modules/tahoma_module.py b/restricted/deviceDiscovery/modules/tahoma_module.py index 5a2a97c..75b9c3b 100644 --- a/restricted/deviceDiscovery/modules/tahoma_module.py +++ b/restricted/deviceDiscovery/modules/tahoma_module.py @@ -1,445 +1,445 @@ -#!/usr/bin/env python3 -""" -Tahoma Module -Enthält NUR Tahoma-spezifische Geräte-Discovery Logik -KEINE Datenbank-Operationen! -""" - -import requests -import urllib3 -import re -import logging -from typing import List, Dict, Optional, Tuple -from modules.base_module import BaseModule - -# SSL-Warnungen deaktivieren -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - -logger = logging.getLogger(__name__) - - -class TahomaAPI: - """Original TahomaAPI Klasse - unverändert""" - - def __init__(self, gateway_ip: str, api_token: str): - self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI" - self.headers = { - "Authorization": f"Bearer {api_token}", - "Content-Type": "application/json" - } - - def get_setup(self) -> Optional[Dict]: - try: - url = f"{self.base_url}/setup" - response = requests.get(url, headers=self.headers, verify=False, timeout=10) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}") - return None - - def get_devices(self) -> List[Dict]: - setup = self.get_setup() - if not setup: - return [] - devices = setup.get('devices', []) - logger.info(f"{len(devices)} Tahoma-Geräte gefunden") - return devices - - -class DeviceClassifier: - """Original DeviceClassifier - unverändert""" - - ACTOR_TYPES = { - 'RollerShutter': 'Rollladen', - 'ExteriorScreen': 'Außenrollo', - 'Awning': 'Markise', - 'ExteriorVenetianBlind':'Außenjalousie', - 'Blind': 'Jalousie', - 'GarageDoor': 'Garagentor', - 'Window': 'Fenster', - 'Light': 'Beleuchtung', - 'OnOff': 'Schalter', - 'DimmableLight': 'Dimmbare Beleuchtung', - 'HeatingSystem': 'Heizungssystem', - 'Valve': 'Ventil', - 'Switch': 'Schalter', - 'Door': 'Tür', - 'Curtain': 'Vorhang', - 'VenetianBlind': 'Jalousie', - 'PergolaScreen': 'Pergola-Rollo' - } - - SENSOR_TYPES = { - 'TemperatureSensor': 'Temperatur', - 'LightSensor': 'Licht', - 'HumiditySensor': 'Feuchtigkeit', - 'ContactSensor': 'Kontakt', - 'OccupancySensor': 'Anwesenheit', - 'SmokeSensor': 'Rauch', - 'WaterDetectionSensor': 'Wasser', - 'WindowHandle': 'Fenstergriff', - 'MotionSensor': 'Bewegung', - 'SunSensor': 'Sonne', - 'WindSensor': 'Wind', - 'RainSensor': 'Regen', - 'ConsumptionSensor': 'Verbrauch' - } - - STATE_NAMES = { - "core:BatteryLevelState": 'Ladestand', - "core:BatteryState": 'Batteriezustand', - "core:ClosureState": 'Position', - "core:CommandLockLevelsState": 'Gesperrt', - "core:Memorized1OrientationState": 'Gespeicherte Neigung', - "core:Memorized1PositionState": 'Gespeicherte Position', - "core:MovingState": 'Fährt gerade', - "core:OpenClosedState": 'Geöffnet/Geschlossen', - "core:PriorityLockTimerState": 'Verriegelungstimer', - "core:DiscreteRSSILevelState": 'RSSI', - "core:SlateOrientationState": 'Lamellenausrichtung', - "core:StatusState": 'Status', - "core:LuminanceState": 'Helligkeit', - "core:SmokeState": 'Rauch', - "core:SunEnergyState": 'Sonnenenergie', - "core:TemperatureState": 'Temperatur', - "io:MaintenanceRadioPartBatteryState": 'Ladestand Funkmodul', - "io:MaintenanceSensorPartBatteryState": 'Ladestand Sensor', - "core:SensorDefectState": 'Sensor defekt' - } - - STATE_ENUMS = { - "core:BatteryLevelState": ["full","normal","low","verylow"], - "core:BatteryState": ["full","normal","low","verylow"], - "core:ClosureState": '', - "core:CommandLockLevelsState": '', - "core:Memorized1OrientationState": '', - "core:Memorized1PositionState": '', - "core:MovingState": ["true","false"], - "core:OpenClosedState": ["open","close"], - "core:PriorityLockTimerState": '', - "core:DiscreteRSSILevelState": ["good","normal","low"], - "core:SlateOrientationState": '', - "core:StatusState": ["available","unavailable"], - "core:TargetClosureState": '', - "core:LuminanceState": 'Helligkeit', - "core:SmokeState": ["notDetected","detected"], - "core:SunEnergyState": '', - "core:TemperatureState": '', - "io:MaintenanceRadioPartBatteryState": ["full","normal","low","verylow"], - "io:MaintenanceSensorPartBatteryState": ["full","normal","low","verylow"], - "core:SensorDefectState": ["true","false"] - } - - """ - "core:TargetClosureState": 'Zielposition', - "io:PriorityLockOriginatorState": 'Sperrendes Gerät', - "core:ManufacturerDiagnosticsState": 'Diagnose', - "core:OpenClosedUnknownState": 'Statusanzeige wenn unbekannt', - "core:ManufacturerSettingsState": 'Einstellungen', - "core:RSSILevelState": 'RSSI', - "core:NameState": 'Name', - "core:SecuredPositionState": 'Sicherer Zustand', - """ - - @classmethod - def get_sensor_type_translation(cls, sensor): - return cls.SENSOR_TYPES.get(sensor, sensor) - - @classmethod - def get_actor_type_translation(cls, actor_type): - return cls.ACTOR_TYPES.get(actor_type, actor_type) - - PARAMETER_TYPES = { - 0: "bool", - 1: "integer", # Integer value - 2: "float", # Floating point number - 3: "string", # Text string - 4: "undefined", - 6: "bool", - 11: "array" - } - - TAHOMA_COMMANDS = { - "setClosure": 'Position', - "setClosureAndOrientation": "Position+Neigung", - "setOrientation": "Neigung", - "up": "Auf", - "down": "Zu", - "my": "my-Position anfahren", - "stop": "Stop", - "refresh": "Aktualisieren", - "wink": "Winken", - "setMyPosition": "my-Position einstellen", - "on": "Anschalten", - "off": "Ausschalten", - "toggle": "Toggle", - "setIntensity": "Intensität", - "setColor": "Farbe", - "setColorTemperature": "Farbtemperatur", - "setTargetTemperature": "Solltemperatur", - "setMode": "Modus", - "pulse": "Impuls", - "setLevel": "Wert", - "trigger": "Trigger", - } - # Tahoma Commands mit Parametern - TAHOMA_COMMAND_PARAMS = { - "setClosure": [{"name": "Position","url":"0", "type": "integer", "min": 0, "max": 100}], - "setClosureAndOrientation": [ - {"name": "Position", "url":"0", "type": "integer", "min": 0, "max": 100}, - {"name": "Neigung", "url":"1", "type": "integer", "min": 0, "max": 100} - ], - "setOrientation": [{"name": "Neigung", "url":"0", "type": "integer", "min": 0, "max": 100}], - "up": [], "down": [], "my": [], "stop": [], "refresh": [], "wink":[], - "setMyPosition": [{"name": "Position", "url":"0", "type": "integer", "min": 0, "max": 100}], - "on": [], "off": [], "toggle": [], - "setIntensity": [{"name": "Helligkeit", "url":"0", "type": "integer", "min": 0, "max": 100}], - "setColor": [ - {"name": "Farbton", "url":"0", "type": "integer", "min": 0, "max": 360}, - {"name": "Sättigung", "url":"1", "type": "integer", "min": 0, "max": 100} - ], - "setColorTemperature": [{"name": "Farbtemperatur", "url":"0", "type": "integer", "min": 2000, "max": 6500}], - "setTargetTemperature": [{"name": "Temperatur", "url":"0", "type": "float", "min": 5.0, "max": 30.0}], - "setMode": [{"name": "Betriebsart", "url":"0", "type": "string"}], - "pulse": [{"name": "Impulsdauer", "url":"0", "type": "integer", "min": 1, "max": 3600}], - "setLevel": [{"name": "Ausgangslevel", "url":"0", "type": "integer", "min": 0, "max": 100}], - "trigger": [], - } - - @classmethod - def is_actor(cls, device: Dict) -> bool: - """Prüft ob Gerät ein Aktor ist""" - device_type = device.get('definition').get('uiClass', '') - - if device_type in cls.ACTOR_TYPES: - return True - - commands = device.get('definition', {}).get('commands', []) - if commands: - command_names = [cmd.get('commandName', '') for cmd in commands] - actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'} - if any(cmd in actor_commands for cmd in command_names): - return True - - return False - - @classmethod - def is_sensor(cls, device: Dict) -> bool: - """Prüft ob Gerät ein Sensor ist""" - device_type = device.get('definition').get('uiClass', '') - - if device_type in cls.SENSOR_TYPES: - return True - - states = device.get('states', []) - commands = device.get('definition', {}).get('commands', []) - - if states and len(states) > 0 and len(commands) <= 1: - return True - - return False - - @classmethod - def extract_actor_data(cls, device: Dict) -> tuple: - """Extrahiert Commands und States aus Aktor""" - commands = [] - states = [] - - cmd_definitions = device.get('definition', {}).get('commands', []) - - for cmd in cmd_definitions: - command_url = cmd.get('commandName', '') - command_name = cls.TAHOMA_COMMANDS.get(command_url, command_url) - cmd_params = cls.TAHOMA_COMMAND_PARAMS.get(command_url, "Not in List") - if(cmd_params != "Not in List"): #only append command, if it is on of the listed commands, to prevent flooding the DB with bullshit. - command_entry = { - 'command': command_name, - 'url': command_url, - 'parameters': [] - } - - for cmd_param in cmd_params: - param_detail = {'name': cmd_param.get('name', '')} - - if 'type' in cmd_param: - param_detail['type'] = cmd_param['type'] - if 'min' in cmd_param: - param_detail['min'] = cmd_param['min'] - if 'max' in cmd_param: - param_detail['max'] = cmd_param['max'] - if 'values' in cmd_param: - param_detail['values'] = cmd_param['values'] - if 'url' in cmd_param: - param_detail['url'] = cmd_param['url'] - if param_detail['name']: - command_entry['parameters'].append(param_detail) - - commands.append(command_entry) - - # States extrahieren - state_definitions = device.get('states', []) - for state in state_definitions: - state_url = state.get('name', '') - state_name = cls.STATE_NAMES.get(state_url, '') - state_enums = cls.STATE_ENUMS.get(state_url,'') - if state_name: - state_entry = { - 'name': state_name, - 'url': state_url, - 'type': cls.PARAMETER_TYPES.get(state.get('type', 4)), - 'values': state_enums - } - if 'value' in state: - state_entry['current_value'] = state['value'] - states.append(state_entry) - - return commands, states - - @classmethod - def extract_sensor_data(cls, device: Dict) -> list: - """Extrahiert States aus Sensor""" - states = [] - - state_definitions = device.get('states', []) - for state in state_definitions: - state_url = state.get('name', '') - state_name = cls.STATE_NAMES.get(state_url, '') - state_enums = cls.STATE_ENUMS.get(state_url,'') - if state_name: - state_entry = { - 'name': state_name, - 'url': state_url, - 'type': cls.PARAMETER_TYPES.get(state.get('type', 4)), - 'values': state_enums - } - if 'value' in state: - state_entry['current_value'] = state['value'] - states.append(state_entry) - - return states - - -class TahomaModule(BaseModule): - """ - Tahoma Modul - Implementiert BaseModule Interface - Gibt nur Actors/Sensors zurück, KEINE DB-Operationen - """ - - def is_enabled(self) -> bool: - """Prüft ob Tahoma aktiviert ist""" - return (self.config.tahoma_enable and - self.config.tahoma_ip and - self.config.tahoma_token) - - def discover(self) -> Tuple[List[Dict], List[Dict]]: - """ - Führt Tahoma Discovery durch - - Returns: - Tuple (actors, sensors) - Listen von Dicts im vereinheitlichten Format - """ - logger.info("\n" + "=" * 60) - logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN") - logger.info("=" * 60) - - actors = [] - sensors = [] - - # TahomaAPI initialisieren - tahoma = TahomaAPI(self.config.tahoma_ip, self.config.tahoma_token) - devices = tahoma.get_devices() - - if not devices: - logger.warning("Keine Tahoma-Geräte gefunden") - return actors, sensors - - # Geräte gruppieren (Original-Logik) - device_groups = {} - standalone_devices = [] - - for device in devices: - device_url = device.get('deviceURL', '') - match = re.match(r'(.+)#(\d+)$', device_url) - - if match: - base_url = match.group(1) - if base_url not in device_groups: - device_groups[base_url] = [] - device_groups[base_url].append(device) - else: - standalone_devices.append(device) - - # Gruppierte Geräte verarbeiten - for base_url, group_devices in device_groups.items(): - main_device = None - for dev in group_devices: - if dev.get('deviceURL', '').endswith('#1'): - main_device = dev - break - - if not main_device and group_devices: - main_device = group_devices[0] - - main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt' - - for device in group_devices: - if(device.get('label','').startswith("IO (") == False): - actor, sensor = self._process_device(device, device.get('label',main_name)) - else: - actor, sensor = self._process_device(device, main_name) - if actor: - actors.append(actor) - if sensor: - sensors.append(sensor) - - # Standalone Geräte verarbeiten - for device in standalone_devices: - device_name = device.get('label', 'Unbekannt') - actor, sensor = self._process_device(device, device_name) - if actor: - actors.append(actor) - if sensor: - sensors.append(sensor) - - logger.info(f"Tahoma: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden") - return actors, sensors - - def _process_device(self, device: Dict, device_name: str) -> Tuple[Optional[Dict], Optional[Dict]]: - """ - Verarbeitet ein einzelnes Gerät - - Returns: - Tuple (actor_dict or None, sensor_dict or None) - """ - device_url = device.get('deviceURL', '') - device_type = device.get('definition').get('uiClass', 'Unknown') - - is_actor = DeviceClassifier.is_actor(device) - is_sensor = DeviceClassifier.is_sensor(device) - - actor = None - sensor = None - - if is_actor: - commands, states = DeviceClassifier.extract_actor_data(device) - device_type = DeviceClassifier.get_actor_type_translation(device_type) - actor = { - 'type': device_type, - 'name': device_name, - 'url': device_url, - 'commands': commands, - 'states': states - } - - elif is_sensor: - states = DeviceClassifier.extract_sensor_data(device) - device_type = DeviceClassifier.get_sensor_type_translation(device_type) - sensor = { - 'type': device_type, - 'name': device_name, - 'url': device_url, - 'states': states - } - - return actor, sensor +#!/usr/bin/env python3 +""" +Tahoma Module +Enthält NUR Tahoma-spezifische Geräte-Discovery Logik +KEINE Datenbank-Operationen! +""" + +import requests +import urllib3 +import re +import logging +from typing import List, Dict, Optional, Tuple +from modules.base_module import BaseModule + +# SSL-Warnungen deaktivieren +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + + +class TahomaAPI: + """Original TahomaAPI Klasse - unverändert""" + + def __init__(self, gateway_ip: str, api_token: str): + self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI" + self.headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + + def get_setup(self) -> Optional[Dict]: + try: + url = f"{self.base_url}/setup" + response = requests.get(url, headers=self.headers, verify=False, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}") + return None + + def get_devices(self) -> List[Dict]: + setup = self.get_setup() + if not setup: + return [] + devices = setup.get('devices', []) + logger.info(f"{len(devices)} Tahoma-Geräte gefunden") + return devices + + +class DeviceClassifier: + """Original DeviceClassifier - unverändert""" + + ACTOR_TYPES = { + 'RollerShutter': 'Rollladen', + 'ExteriorScreen': 'Außenrollo', + 'Awning': 'Markise', + 'ExteriorVenetianBlind':'Außenjalousie', + 'Blind': 'Jalousie', + 'GarageDoor': 'Garagentor', + 'Window': 'Fenster', + 'Light': 'Beleuchtung', + 'OnOff': 'Schalter', + 'DimmableLight': 'Dimmbare Beleuchtung', + 'HeatingSystem': 'Heizungssystem', + 'Valve': 'Ventil', + 'Switch': 'Schalter', + 'Door': 'Tür', + 'Curtain': 'Vorhang', + 'VenetianBlind': 'Jalousie', + 'PergolaScreen': 'Pergola-Rollo' + } + + SENSOR_TYPES = { + 'TemperatureSensor': 'Temperatur', + 'LightSensor': 'Licht', + 'HumiditySensor': 'Feuchtigkeit', + 'ContactSensor': 'Kontakt', + 'OccupancySensor': 'Anwesenheit', + 'SmokeSensor': 'Rauch', + 'WaterDetectionSensor': 'Wasser', + 'WindowHandle': 'Fenstergriff', + 'MotionSensor': 'Bewegung', + 'SunSensor': 'Sonne', + 'WindSensor': 'Wind', + 'RainSensor': 'Regen', + 'ConsumptionSensor': 'Verbrauch' + } + + STATE_NAMES = { + "core:BatteryLevelState": 'Ladestand', + "core:BatteryState": 'Batteriezustand', + "core:ClosureState": 'Position', + "core:CommandLockLevelsState": 'Gesperrt', + "core:Memorized1OrientationState": 'Gespeicherte Neigung', + "core:Memorized1PositionState": 'Gespeicherte Position', + "core:MovingState": 'Fährt gerade', + "core:OpenClosedState": 'Geöffnet/Geschlossen', + "core:PriorityLockTimerState": 'Verriegelungstimer', + "core:DiscreteRSSILevelState": 'RSSI', + "core:SlateOrientationState": 'Lamellenausrichtung', + "core:StatusState": 'Status', + "core:LuminanceState": 'Helligkeit', + "core:SmokeState": 'Rauch', + "core:SunEnergyState": 'Sonnenenergie', + "core:TemperatureState": 'Temperatur', + "io:MaintenanceRadioPartBatteryState": 'Ladestand Funkmodul', + "io:MaintenanceSensorPartBatteryState": 'Ladestand Sensor', + "core:SensorDefectState": 'Sensor defekt' + } + + STATE_ENUMS = { + "core:BatteryLevelState": ["full","normal","low","verylow"], + "core:BatteryState": ["full","normal","low","verylow"], + "core:ClosureState": '', + "core:CommandLockLevelsState": '', + "core:Memorized1OrientationState": '', + "core:Memorized1PositionState": '', + "core:MovingState": ["true","false"], + "core:OpenClosedState": ["open","close"], + "core:PriorityLockTimerState": '', + "core:DiscreteRSSILevelState": ["good","normal","low"], + "core:SlateOrientationState": '', + "core:StatusState": ["available","unavailable"], + "core:TargetClosureState": '', + "core:LuminanceState": 'Helligkeit', + "core:SmokeState": ["notDetected","detected"], + "core:SunEnergyState": '', + "core:TemperatureState": '', + "io:MaintenanceRadioPartBatteryState": ["full","normal","low","verylow"], + "io:MaintenanceSensorPartBatteryState": ["full","normal","low","verylow"], + "core:SensorDefectState": ["true","false"] + } + + """ + "core:TargetClosureState": 'Zielposition', + "io:PriorityLockOriginatorState": 'Sperrendes Gerät', + "core:ManufacturerDiagnosticsState": 'Diagnose', + "core:OpenClosedUnknownState": 'Statusanzeige wenn unbekannt', + "core:ManufacturerSettingsState": 'Einstellungen', + "core:RSSILevelState": 'RSSI', + "core:NameState": 'Name', + "core:SecuredPositionState": 'Sicherer Zustand', + """ + + @classmethod + def get_sensor_type_translation(cls, sensor): + return cls.SENSOR_TYPES.get(sensor, sensor) + + @classmethod + def get_actor_type_translation(cls, actor_type): + return cls.ACTOR_TYPES.get(actor_type, actor_type) + + PARAMETER_TYPES = { + 0: "bool", + 1: "integer", # Integer value + 2: "float", # Floating point number + 3: "string", # Text string + 4: "undefined", + 6: "bool", + 11: "array" + } + + TAHOMA_COMMANDS = { + "setClosure": 'Position', + "setClosureAndOrientation": "Position+Neigung", + "setOrientation": "Neigung", + "up": "Auf", + "down": "Zu", + "my": "my-Position anfahren", + "stop": "Stop", + "refresh": "Aktualisieren", + "wink": "Winken", + "setMyPosition": "my-Position einstellen", + "on": "Anschalten", + "off": "Ausschalten", + "toggle": "Toggle", + "setIntensity": "Intensität", + "setColor": "Farbe", + "setColorTemperature": "Farbtemperatur", + "setTargetTemperature": "Solltemperatur", + "setMode": "Modus", + "pulse": "Impuls", + "setLevel": "Wert", + "trigger": "Trigger", + } + # Tahoma Commands mit Parametern + TAHOMA_COMMAND_PARAMS = { + "setClosure": [{"name": "Position","url":"0", "type": "integer", "min": 0, "max": 100}], + "setClosureAndOrientation": [ + {"name": "Position", "url":"0", "type": "integer", "min": 0, "max": 100}, + {"name": "Neigung", "url":"1", "type": "integer", "min": 0, "max": 100} + ], + "setOrientation": [{"name": "Neigung", "url":"0", "type": "integer", "min": 0, "max": 100}], + "up": [], "down": [], "my": [], "stop": [], "refresh": [], "wink":[], + "setMyPosition": [{"name": "Position", "url":"0", "type": "integer", "min": 0, "max": 100}], + "on": [], "off": [], "toggle": [], + "setIntensity": [{"name": "Helligkeit", "url":"0", "type": "integer", "min": 0, "max": 100}], + "setColor": [ + {"name": "Farbton", "url":"0", "type": "integer", "min": 0, "max": 360}, + {"name": "Sättigung", "url":"1", "type": "integer", "min": 0, "max": 100} + ], + "setColorTemperature": [{"name": "Farbtemperatur", "url":"0", "type": "integer", "min": 2000, "max": 6500}], + "setTargetTemperature": [{"name": "Temperatur", "url":"0", "type": "float", "min": 5.0, "max": 30.0}], + "setMode": [{"name": "Betriebsart", "url":"0", "type": "string"}], + "pulse": [{"name": "Impulsdauer", "url":"0", "type": "integer", "min": 1, "max": 3600}], + "setLevel": [{"name": "Ausgangslevel", "url":"0", "type": "integer", "min": 0, "max": 100}], + "trigger": [], + } + + @classmethod + def is_actor(cls, device: Dict) -> bool: + """Prüft ob Gerät ein Aktor ist""" + device_type = device.get('definition').get('uiClass', '') + + if device_type in cls.ACTOR_TYPES: + return True + + commands = device.get('definition', {}).get('commands', []) + if commands: + command_names = [cmd.get('commandName', '') for cmd in commands] + actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'} + if any(cmd in actor_commands for cmd in command_names): + return True + + return False + + @classmethod + def is_sensor(cls, device: Dict) -> bool: + """Prüft ob Gerät ein Sensor ist""" + device_type = device.get('definition').get('uiClass', '') + + if device_type in cls.SENSOR_TYPES: + return True + + states = device.get('states', []) + commands = device.get('definition', {}).get('commands', []) + + if states and len(states) > 0 and len(commands) <= 1: + return True + + return False + + @classmethod + def extract_actor_data(cls, device: Dict) -> tuple: + """Extrahiert Commands und States aus Aktor""" + commands = [] + states = [] + + cmd_definitions = device.get('definition', {}).get('commands', []) + + for cmd in cmd_definitions: + command_url = cmd.get('commandName', '') + command_name = cls.TAHOMA_COMMANDS.get(command_url, command_url) + cmd_params = cls.TAHOMA_COMMAND_PARAMS.get(command_url, "Not in List") + if(cmd_params != "Not in List"): #only append command, if it is on of the listed commands, to prevent flooding the DB with bullshit. + command_entry = { + 'command': command_name, + 'url': command_url, + 'parameters': [] + } + + for cmd_param in cmd_params: + param_detail = {'name': cmd_param.get('name', '')} + + if 'type' in cmd_param: + param_detail['type'] = cmd_param['type'] + if 'min' in cmd_param: + param_detail['min'] = cmd_param['min'] + if 'max' in cmd_param: + param_detail['max'] = cmd_param['max'] + if 'values' in cmd_param: + param_detail['values'] = cmd_param['values'] + if 'url' in cmd_param: + param_detail['url'] = cmd_param['url'] + if param_detail['name']: + command_entry['parameters'].append(param_detail) + + commands.append(command_entry) + + # States extrahieren + state_definitions = device.get('states', []) + for state in state_definitions: + state_url = state.get('name', '') + state_name = cls.STATE_NAMES.get(state_url, '') + state_enums = cls.STATE_ENUMS.get(state_url,'') + if state_name: + state_entry = { + 'name': state_name, + 'url': state_url, + 'type': cls.PARAMETER_TYPES.get(state.get('type', 4)), + 'values': state_enums + } + if 'value' in state: + state_entry['current_value'] = state['value'] + states.append(state_entry) + + return commands, states + + @classmethod + def extract_sensor_data(cls, device: Dict) -> list: + """Extrahiert States aus Sensor""" + states = [] + + state_definitions = device.get('states', []) + for state in state_definitions: + state_url = state.get('name', '') + state_name = cls.STATE_NAMES.get(state_url, '') + state_enums = cls.STATE_ENUMS.get(state_url,'') + if state_name: + state_entry = { + 'name': state_name, + 'url': state_url, + 'type': cls.PARAMETER_TYPES.get(state.get('type', 4)), + 'values': state_enums + } + if 'value' in state: + state_entry['current_value'] = state['value'] + states.append(state_entry) + + return states + + +class TahomaModule(BaseModule): + """ + Tahoma Modul - Implementiert BaseModule Interface + Gibt nur Actors/Sensors zurück, KEINE DB-Operationen + """ + + def is_enabled(self) -> bool: + """Prüft ob Tahoma aktiviert ist""" + return (self.config.tahoma_enable and + self.config.tahoma_ip and + self.config.tahoma_token) + + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Führt Tahoma Discovery durch + + Returns: + Tuple (actors, sensors) - Listen von Dicts im vereinheitlichten Format + """ + logger.info("\n" + "=" * 60) + logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN") + logger.info("=" * 60) + + actors = [] + sensors = [] + + # TahomaAPI initialisieren + tahoma = TahomaAPI(self.config.tahoma_ip, self.config.tahoma_token) + devices = tahoma.get_devices() + + if not devices: + logger.warning("Keine Tahoma-Geräte gefunden") + return actors, sensors + + # Geräte gruppieren (Original-Logik) + device_groups = {} + standalone_devices = [] + + for device in devices: + device_url = device.get('deviceURL', '') + match = re.match(r'(.+)#(\d+)$', device_url) + + if match: + base_url = match.group(1) + if base_url not in device_groups: + device_groups[base_url] = [] + device_groups[base_url].append(device) + else: + standalone_devices.append(device) + + # Gruppierte Geräte verarbeiten + for base_url, group_devices in device_groups.items(): + main_device = None + for dev in group_devices: + if dev.get('deviceURL', '').endswith('#1'): + main_device = dev + break + + if not main_device and group_devices: + main_device = group_devices[0] + + main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt' + + for device in group_devices: + if(device.get('label','').startswith("IO (") == False): + actor, sensor = self._process_device(device, device.get('label',main_name)) + else: + actor, sensor = self._process_device(device, main_name) + if actor: + actors.append(actor) + if sensor: + sensors.append(sensor) + + # Standalone Geräte verarbeiten + for device in standalone_devices: + device_name = device.get('label', 'Unbekannt') + actor, sensor = self._process_device(device, device_name) + if actor: + actors.append(actor) + if sensor: + sensors.append(sensor) + + logger.info(f"Tahoma: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden") + return actors, sensors + + def _process_device(self, device: Dict, device_name: str) -> Tuple[Optional[Dict], Optional[Dict]]: + """ + Verarbeitet ein einzelnes Gerät + + Returns: + Tuple (actor_dict or None, sensor_dict or None) + """ + device_url = device.get('deviceURL', '') + device_type = device.get('definition').get('uiClass', 'Unknown') + + is_actor = DeviceClassifier.is_actor(device) + is_sensor = DeviceClassifier.is_sensor(device) + + actor = None + sensor = None + + if is_actor: + commands, states = DeviceClassifier.extract_actor_data(device) + device_type = DeviceClassifier.get_actor_type_translation(device_type) + actor = { + 'type': device_type, + 'name': device_name, + 'url': device_url, + 'commands': commands, + 'states': states + } + + elif is_sensor: + states = DeviceClassifier.extract_sensor_data(device) + device_type = DeviceClassifier.get_sensor_type_translation(device_type) + sensor = { + 'type': device_type, + 'name': device_name, + 'url': device_url, + 'states': states + } + + return actor, sensor diff --git a/restricted/deviceDiscovery/modules/wled_module.py b/restricted/deviceDiscovery/modules/wled_module.py index 43af000..9f56bcd 100644 --- a/restricted/deviceDiscovery/modules/wled_module.py +++ b/restricted/deviceDiscovery/modules/wled_module.py @@ -1,316 +1,316 @@ -#!/usr/bin/env python3 -""" -WLED Module -Enthält NUR WLED-spezifische Geräte-Discovery Logik -KEINE Datenbank-Operationen! -""" - -import requests -import socket -import concurrent.futures -import logging -from typing import List, Dict, Optional, Tuple -from modules.base_module import BaseModule - -logger = logging.getLogger(__name__) - - -class WLEDDiscovery: - """Original WLEDDiscovery Klasse - unverändert""" - - @staticmethod - def discover_devices(timeout: int = 5) -> List[str]: - """Sucht nach WLED-Geräten via mDNS""" - try: - from zeroconf import ServiceBrowser, ServiceListener, Zeroconf - import time - - class WLEDListener(ServiceListener): - def __init__(self): - self.devices = [] - - def add_service(self, zc, type_, name): - info = zc.get_service_info(type_, name) - if info: - addresses = [socket.inet_ntoa(addr) for addr in info.addresses] - for addr in addresses: - if addr not in self.devices: - self.devices.append(addr) - logger.info(f"WLED-Gerät gefunden: {name} ({addr})") - - def remove_service(self, zc, type_, name): - pass - - def update_service(self, zc, type_, name): - pass - - zeroconf = Zeroconf() - listener = WLEDListener() - browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener) - - logger.info(f"Suche nach WLED-Geräten (Timeout: {timeout}s)...") - time.sleep(timeout) - - zeroconf.close() - - # Nur WLED-Geräte filtern - wled_devices = [] - for ip in listener.devices: - if WLEDDiscovery.is_wled_device(ip): - wled_devices.append(ip) - - logger.info(f"{len(wled_devices)} WLED-Geräte gefunden") - return wled_devices - - except ImportError: - logger.warning("zeroconf nicht installiert. Verwende Netzwerk-Scan...") - return WLEDDiscovery.scan_network() - except Exception as e: - logger.error(f"Fehler bei WLED-Discovery: {e}") - return [] - - @staticmethod - def scan_network(network: str = None, max_threads: int = 50) -> List[str]: - """Scannt das Netzwerk nach WLED-Geräten""" - if network is None: - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - local_ip = s.getsockname()[0] - s.close() - network_prefix = '.'.join(local_ip.split('.')[:-1]) - except: - logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x") - network_prefix = "192.168.1" - else: - network_prefix = '.'.join(network.split('.')[:3]) - - logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach WLED-Geräten...") - - def check_ip(ip): - if WLEDDiscovery.is_wled_device(ip): - return ip - return None - - wled_devices = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor: - futures = [executor.submit(check_ip, f"{network_prefix}.{i}") - for i in range(1, 255)] - - for future in concurrent.futures.as_completed(futures): - result = future.result() - if result: - wled_devices.append(result) - logger.info(f"WLED-Gerät gefunden: {result}") - - return wled_devices - - @staticmethod - def is_wled_device(ip: str, timeout: float = 2.0) -> bool: - """Prüft ob IP ein WLED-Gerät ist""" - try: - response = requests.get( - f"http://{ip}/json/info", - timeout=timeout, - headers={'User-Agent': 'DeviceDiscovery/1.0'} - ) - if response.status_code == 200: - data = response.json() - return 'ver' in data or 'name' in data - except: - pass - return False - - -class WLEDAPI: - """Original WLEDAPI Klasse - unverändert""" - - def __init__(self, ip: str): - self.ip = ip - self.base_url = f"http://{ip}" - - def get_info(self) -> Optional[Dict]: - try: - response = requests.get(f"{self.base_url}/json/info", timeout=2) - response.raise_for_status() - return response.json() - except Exception as e: - logger.error(f"Fehler beim Abrufen der WLED-Info von {self.ip}: {e}") - return None - - def get_presets(self) -> Optional[List]: - try: - response = requests.get(f"{self.base_url}/presets.json", timeout=2) - response.raise_for_status() - presets_data = response.json() - preset_list = [] - if isinstance(presets_data, dict): - for preset_id, preset_data in presets_data.items(): - preset_name = preset_data.get('n', f'Preset {preset_id}') - preset_list.append({int(preset_id): preset_name}) - return preset_list - except Exception as e: - logger.error(f"Fehler beim Abrufen der WLED-Presets von {self.ip}: {e}") - return None - - def get_effects(self) -> Optional[List]: - try: - response = requests.get(f"{self.base_url}/json/eff", timeout=2) - response.raise_for_status() - eff_data = response.json() - eff_list = [] - for eff_id, eff_name in enumerate(eff_data): - if not eff_name: - eff_name = f"Effect {eff_id}" - eff_list.append({int(eff_id): eff_name}) - return eff_list - except Exception as e: - logger.error(f"Fehler beim Abrufen der WLED-Effects von {self.ip}: {e}") - return None - - def get_state(self) -> Optional[Dict]: - try: - response = requests.get(f"{self.base_url}/json/state", timeout=2) - response.raise_for_status() - return response.json() - except Exception as e: - logger.error(f"Fehler beim Abrufen des WLED-State von {self.ip}: {e}") - return None - - def get_device_data(self) -> Optional[Dict]: - """Erstellt Geräte-Dict im vereinheitlichten Format""" - info = self.get_info() - state = self.get_state() - - if not info: - return None - - name = info.get('name', f"WLED {self.ip}") - preset_values = self.get_presets() - eff_values = self.get_effects() - - commands = [ - {'command': 'An', 'url': '{"on":true}', 'parameters': []}, - {'command': 'Aus', 'url': '{"on":false}', 'parameters': []}, - { - 'command': 'Helligkeit', 'url': '{"bri":%brightness%}', - 'parameters': [ - {'name': 'brightness', 'type': 'integer', 'min': 0, 'max': 255} - ] - }, - { - 'command': 'Farbe', 'url': '{"seg":[{"col":[[%red%,%green%,%blue%]]}]}', - 'parameters': [ - {'name': 'red', 'type': 'integer', 'min': 0, 'max': 255}, - {'name': 'green', 'type': 'integer', 'min': 0, 'max': 255}, - {'name': 'blue', 'type': 'integer', 'min': 0, 'max': 255} - ] - }, - { - 'command': 'Effekt', 'url': '{"seg":[{"fx":%effect%}]}', - 'parameters': [ - {'name': 'effect', 'type': 'integer', 'min': 0, 'max': 255, 'values': eff_values} - ] - }, - { - 'command': 'Preset', 'url': '{"ps":%preset%}', - 'parameters': [ - {'name': 'preset', 'type': 'integer', 'min': 1, 'max': 250, 'values': preset_values} - ] - } - ] - - states = [] - if state: - states.append({ - 'name': 'An', - 'url': 'on', - 'type': 'boolean', - 'current_value': state.get('on', False) - }) - states.append({ - 'name': 'Helligkeit', - 'url': 'bri', - 'type': 'integer', - 'current_value': state.get('bri', 0) - }) - - segments = state.get('seg', []) - if segments and len(segments) > 0: - colors = segments[0].get('col', [[0,0,0]]) - if colors and len(colors) > 0: - states.append({ - 'name': 'Farbe', - 'url': 'seg[0].col[0]', - 'type': 'array', - 'current_value': colors[0] - }) - - return { - 'type': 'WLED', - 'name': name, - 'url': f"wled://{self.ip}", - 'commands': commands, - 'states': states - } - - -class WLEDModule(BaseModule): - """ - WLED Modul - Implementiert BaseModule Interface - Gibt nur Actors zurück, KEINE DB-Operationen - """ - - def is_enabled(self) -> bool: - """Prüft ob WLED aktiviert ist""" - return self.config.wled_enable - - def discover(self) -> Tuple[List[Dict], List[Dict]]: - """ - Führt WLED Discovery durch - - Returns: - Tuple (actors, sensors) - WLED sind immer Actors - """ - logger.info("\n" + "=" * 60) - logger.info("WLED-GERÄTE WERDEN GESUCHT") - logger.info("=" * 60) - - actors = [] - sensors = [] - - # Discovery - wled_ips = WLEDDiscovery.discover_devices(timeout=self.config.wled_discovery_timeout) - - # Manuelle IPs hinzufügen - if self.config.wled_manual_ips: - logger.info(f"Füge {len(self.config.wled_manual_ips)} manuelle WLED-IPs hinzu...") - for manual_ip in self.config.wled_manual_ips: - if manual_ip not in wled_ips: - if WLEDDiscovery.is_wled_device(manual_ip): - wled_ips.append(manual_ip) - logger.info(f"✓ Manuelles WLED-Gerät: {manual_ip}") - else: - logger.warning(f"⚠ {manual_ip} ist kein WLED-Gerät") - - if not wled_ips: - logger.info("Keine WLED-Geräte gefunden") - return actors, sensors - - logger.info(f"{len(wled_ips)} WLED-Geräte gefunden") - - # Gerätedaten abrufen - for ip in wled_ips: - try: - wled = WLEDAPI(ip) - device_data = wled.get_device_data() - - if device_data: - actors.append(device_data) - else: - logger.warning(f"⚠ Konnte keine Daten von WLED {ip} abrufen") - except Exception as e: - logger.error(f"✗ Fehler beim Verarbeiten von WLED {ip}: {e}") - - logger.info(f"WLED: {len(actors)} Aktoren gefunden") - return actors, sensors +#!/usr/bin/env python3 +""" +WLED Module +Enthält NUR WLED-spezifische Geräte-Discovery Logik +KEINE Datenbank-Operationen! +""" + +import requests +import socket +import concurrent.futures +import logging +from typing import List, Dict, Optional, Tuple +from modules.base_module import BaseModule + +logger = logging.getLogger(__name__) + + +class WLEDDiscovery: + """Original WLEDDiscovery Klasse - unverändert""" + + @staticmethod + def discover_devices(timeout: int = 5) -> List[str]: + """Sucht nach WLED-Geräten via mDNS""" + try: + from zeroconf import ServiceBrowser, ServiceListener, Zeroconf + import time + + class WLEDListener(ServiceListener): + def __init__(self): + self.devices = [] + + def add_service(self, zc, type_, name): + info = zc.get_service_info(type_, name) + if info: + addresses = [socket.inet_ntoa(addr) for addr in info.addresses] + for addr in addresses: + if addr not in self.devices: + self.devices.append(addr) + logger.info(f"WLED-Gerät gefunden: {name} ({addr})") + + def remove_service(self, zc, type_, name): + pass + + def update_service(self, zc, type_, name): + pass + + zeroconf = Zeroconf() + listener = WLEDListener() + browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener) + + logger.info(f"Suche nach WLED-Geräten (Timeout: {timeout}s)...") + time.sleep(timeout) + + zeroconf.close() + + # Nur WLED-Geräte filtern + wled_devices = [] + for ip in listener.devices: + if WLEDDiscovery.is_wled_device(ip): + wled_devices.append(ip) + + logger.info(f"{len(wled_devices)} WLED-Geräte gefunden") + return wled_devices + + except ImportError: + logger.warning("zeroconf nicht installiert. Verwende Netzwerk-Scan...") + return WLEDDiscovery.scan_network() + except Exception as e: + logger.error(f"Fehler bei WLED-Discovery: {e}") + return [] + + @staticmethod + def scan_network(network: str = None, max_threads: int = 50) -> List[str]: + """Scannt das Netzwerk nach WLED-Geräten""" + if network is None: + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + s.close() + network_prefix = '.'.join(local_ip.split('.')[:-1]) + except: + logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x") + network_prefix = "192.168.1" + else: + network_prefix = '.'.join(network.split('.')[:3]) + + logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach WLED-Geräten...") + + def check_ip(ip): + if WLEDDiscovery.is_wled_device(ip): + return ip + return None + + wled_devices = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor: + futures = [executor.submit(check_ip, f"{network_prefix}.{i}") + for i in range(1, 255)] + + for future in concurrent.futures.as_completed(futures): + result = future.result() + if result: + wled_devices.append(result) + logger.info(f"WLED-Gerät gefunden: {result}") + + return wled_devices + + @staticmethod + def is_wled_device(ip: str, timeout: float = 2.0) -> bool: + """Prüft ob IP ein WLED-Gerät ist""" + try: + response = requests.get( + f"http://{ip}/json/info", + timeout=timeout, + headers={'User-Agent': 'DeviceDiscovery/1.0'} + ) + if response.status_code == 200: + data = response.json() + return 'ver' in data or 'name' in data + except: + pass + return False + + +class WLEDAPI: + """Original WLEDAPI Klasse - unverändert""" + + def __init__(self, ip: str): + self.ip = ip + self.base_url = f"http://{ip}" + + def get_info(self) -> Optional[Dict]: + try: + response = requests.get(f"{self.base_url}/json/info", timeout=2) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Fehler beim Abrufen der WLED-Info von {self.ip}: {e}") + return None + + def get_presets(self) -> Optional[List]: + try: + response = requests.get(f"{self.base_url}/presets.json", timeout=2) + response.raise_for_status() + presets_data = response.json() + preset_list = [] + if isinstance(presets_data, dict): + for preset_id, preset_data in presets_data.items(): + preset_name = preset_data.get('n', f'Preset {preset_id}') + preset_list.append({int(preset_id): preset_name}) + return preset_list + except Exception as e: + logger.error(f"Fehler beim Abrufen der WLED-Presets von {self.ip}: {e}") + return None + + def get_effects(self) -> Optional[List]: + try: + response = requests.get(f"{self.base_url}/json/eff", timeout=2) + response.raise_for_status() + eff_data = response.json() + eff_list = [] + for eff_id, eff_name in enumerate(eff_data): + if not eff_name: + eff_name = f"Effect {eff_id}" + eff_list.append({int(eff_id): eff_name}) + return eff_list + except Exception as e: + logger.error(f"Fehler beim Abrufen der WLED-Effects von {self.ip}: {e}") + return None + + def get_state(self) -> Optional[Dict]: + try: + response = requests.get(f"{self.base_url}/json/state", timeout=2) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Fehler beim Abrufen des WLED-State von {self.ip}: {e}") + return None + + def get_device_data(self) -> Optional[Dict]: + """Erstellt Geräte-Dict im vereinheitlichten Format""" + info = self.get_info() + state = self.get_state() + + if not info: + return None + + name = info.get('name', f"WLED {self.ip}") + preset_values = self.get_presets() + eff_values = self.get_effects() + + commands = [ + {'command': 'An', 'url': '{"on":true}', 'parameters': []}, + {'command': 'Aus', 'url': '{"on":false}', 'parameters': []}, + { + 'command': 'Helligkeit', 'url': '{"bri":%brightness%}', + 'parameters': [ + {'name': 'brightness', 'type': 'integer', 'min': 0, 'max': 255} + ] + }, + { + 'command': 'Farbe', 'url': '{"seg":[{"col":[[%red%,%green%,%blue%]]}]}', + 'parameters': [ + {'name': 'red', 'type': 'integer', 'min': 0, 'max': 255}, + {'name': 'green', 'type': 'integer', 'min': 0, 'max': 255}, + {'name': 'blue', 'type': 'integer', 'min': 0, 'max': 255} + ] + }, + { + 'command': 'Effekt', 'url': '{"seg":[{"fx":%effect%}]}', + 'parameters': [ + {'name': 'effect', 'type': 'integer', 'min': 0, 'max': 255, 'values': eff_values} + ] + }, + { + 'command': 'Preset', 'url': '{"ps":%preset%}', + 'parameters': [ + {'name': 'preset', 'type': 'integer', 'min': 1, 'max': 250, 'values': preset_values} + ] + } + ] + + states = [] + if state: + states.append({ + 'name': 'An', + 'url': 'on', + 'type': 'boolean', + 'current_value': state.get('on', False) + }) + states.append({ + 'name': 'Helligkeit', + 'url': 'bri', + 'type': 'integer', + 'current_value': state.get('bri', 0) + }) + + segments = state.get('seg', []) + if segments and len(segments) > 0: + colors = segments[0].get('col', [[0,0,0]]) + if colors and len(colors) > 0: + states.append({ + 'name': 'Farbe', + 'url': 'seg[0].col[0]', + 'type': 'array', + 'current_value': colors[0] + }) + + return { + 'type': 'WLED', + 'name': name, + 'url': f"wled://{self.ip}", + 'commands': commands, + 'states': states + } + + +class WLEDModule(BaseModule): + """ + WLED Modul - Implementiert BaseModule Interface + Gibt nur Actors zurück, KEINE DB-Operationen + """ + + def is_enabled(self) -> bool: + """Prüft ob WLED aktiviert ist""" + return self.config.wled_enable + + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Führt WLED Discovery durch + + Returns: + Tuple (actors, sensors) - WLED sind immer Actors + """ + logger.info("\n" + "=" * 60) + logger.info("WLED-GERÄTE WERDEN GESUCHT") + logger.info("=" * 60) + + actors = [] + sensors = [] + + # Discovery + wled_ips = WLEDDiscovery.discover_devices(timeout=self.config.wled_discovery_timeout) + + # Manuelle IPs hinzufügen + if self.config.wled_manual_ips: + logger.info(f"Füge {len(self.config.wled_manual_ips)} manuelle WLED-IPs hinzu...") + for manual_ip in self.config.wled_manual_ips: + if manual_ip not in wled_ips: + if WLEDDiscovery.is_wled_device(manual_ip): + wled_ips.append(manual_ip) + logger.info(f"✓ Manuelles WLED-Gerät: {manual_ip}") + else: + logger.warning(f"⚠ {manual_ip} ist kein WLED-Gerät") + + if not wled_ips: + logger.info("Keine WLED-Geräte gefunden") + return actors, sensors + + logger.info(f"{len(wled_ips)} WLED-Geräte gefunden") + + # Gerätedaten abrufen + for ip in wled_ips: + try: + wled = WLEDAPI(ip) + device_data = wled.get_device_data() + + if device_data: + actors.append(device_data) + else: + logger.warning(f"⚠ Konnte keine Daten von WLED {ip} abrufen") + except Exception as e: + logger.error(f"✗ Fehler beim Verarbeiten von WLED {ip}: {e}") + + logger.info(f"WLED: {len(actors)} Aktoren gefunden") + return actors, sensors diff --git a/restricted/header.php b/restricted/header.php index d9f821d..75db266 100644 --- a/restricted/header.php +++ b/restricted/header.php @@ -1,208 +1,208 @@ - - - - - - - - Smarthome control - - - - - - - - - - - - - - - - - - - - " /> - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - + + + + + + + + Smarthome control + + + + + + + + + + + + + + + + + + + + " /> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/restricted/history/getTahomaDevices.py b/restricted/history/getTahomaDevices.py index d01fe53..00c1475 100644 --- a/restricted/history/getTahomaDevices.py +++ b/restricted/history/getTahomaDevices.py @@ -1,1573 +1,1573 @@ -#!/usr/bin/env python3 -""" -Somfy Tahoma Local API to MySQL Database Script -Liest alle Aktoren und Sensoren aus der Tahoma Box und speichert sie in MySQL -""" - -import requests -import pymysql -from pymysql import Error -import json -import logging -from typing import List, Dict, Optional -import urllib3 -import socket -import configparser -import os -from mqtt_discovery import HomeAssistantDiscovery, MQTTDeviceConverter - -# SSL-Warnungen deaktivieren (Tahoma verwendet selbst-signierte Zertifikate) -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - -class Config: - """Lädt und verwaltet die Konfiguration aus config.ini""" - - def __init__(self, config_file: str = 'config.ini'): - """ - Lädt die Konfiguration - - Args: - config_file: Name der Konfigurationsdatei (wird im Script-Verzeichnis gesucht) - """ - # Verzeichnis des Scripts ermitteln - script_dir = os.path.dirname(os.path.abspath(__file__)) - config_path = os.path.join(script_dir, config_file) - - if not os.path.exists(config_path): - raise FileNotFoundError( - f"Konfigurationsdatei '{config_file}' nicht gefunden!\n" - f"Erwartet in: {script_dir}\n" - f"Bitte erstellen Sie die Datei anhand der Vorlage config.ini.example" - ) - - self.config = configparser.ConfigParser() - self.config.read(config_path, encoding='utf-8') - - logger.info(f"Konfiguration geladen von: {config_path}") - - # Konfiguration validieren - self._validate() - - def _validate(self): - """Validiert die Konfiguration""" - required_sections = ['tahoma', 'database', 'wled', 'options'] - for section in required_sections: - if not self.config.has_section(section): - raise ValueError(f"Erforderliche Sektion '[{section}]' fehlt in config.ini") - - def _get_bool(self, section: str, key: str, fallback: bool = False) -> bool: - """Hilfsmethode zum Lesen von Boolean-Werten""" - value = self.config.get(section, key, fallback=str(fallback)).strip().lower() - return value in ('true', '1', 'yes', 'on') - - def _get_int(self, section: str, key: str, fallback: int = 0) -> int: - """Hilfsmethode zum Lesen von Integer-Werten""" - try: - return self.config.getint(section, key, fallback=fallback) - except ValueError: - return fallback - - def _get_list(self, section: str, key: str) -> List[str]: - """Hilfsmethode zum Lesen von Listen (komma-getrennt)""" - value = self.config.get(section, key, fallback='').strip() - if not value: - return [] - return [item.strip() for item in value.split(',') if item.strip()] - - # Tahoma Konfiguration - @property - def tahoma_ip(self) -> str: - return self.config.get('tahoma', 'ip').strip() - - @property - def tahoma_token(self) -> str: - return self.config.get('tahoma', 'token').strip() - - @property - def tahoma_timeout(self) -> int: - if self.config.has_section('advanced'): - return self._get_int('advanced', 'tahoma_timeout', 10) - return 10 - - # Datenbank Konfiguration - @property - def db_host(self) -> str: - return self.config.get('database', 'host').strip() - - @property - def db_port(self) -> int: - return self._get_int('database', 'port', 3306) - - @property - def db_name(self) -> str: - return self.config.get('database', 'database').strip() - - @property - def db_user(self) -> str: - return self.config.get('database', 'user').strip() - - @property - def db_password(self) -> str: - return self.config.get('database', 'password').strip() - - # WLED Konfiguration - @property - def wled_enable(self) -> bool: - return self._get_bool('wled', 'enable', True) - - @property - def wled_discovery_timeout(self) -> int: - return self._get_int('wled', 'discovery_timeout', 5) - - @property - def wled_manual_ips(self) -> List[str]: - return self._get_list('wled', 'manual_ips') - - @property - def wled_scan_network(self) -> Optional[str]: - value = self.config.get('wled', 'scan_network', fallback='').strip() - return value if value else None - - @property - def wled_timeout(self) -> int: - if self.config.has_section('advanced'): - return self._get_int('advanced', 'wled_timeout', 2) - return 2 - # MQTT Konfiguration - @property - def mqtt_enable(self) -> bool: - if not self.config.has_section('mqtt'): - return False - return self._get_bool('mqtt', 'enable', False) - - @property - def mqtt_broker(self) -> str: - if not self.config.has_section('mqtt'): - return 'localhost' - return self.config.get('mqtt', 'broker', fallback='localhost').strip() - - @property - def mqtt_port(self) -> int: - if not self.config.has_section('mqtt'): - return 1883 - return self._get_int('mqtt', 'port', 1883) - - @property - def mqtt_username(self) -> Optional[str]: - if not self.config.has_section('mqtt'): - return None - value = self.config.get('mqtt', 'username', fallback='').strip() - return value if value else None - - @property - def mqtt_password(self) -> Optional[str]: - if not self.config.has_section('mqtt'): - return None - value = self.config.get('mqtt', 'password', fallback='').strip() - return value if value else None - - @property - def mqtt_discovery_prefix(self) -> str: - if not self.config.has_section('mqtt'): - return 'homeassistant' - return self.config.get('mqtt', 'discovery_prefix', fallback='homeassistant').strip() - - @property - def mqtt_discovery_timeout(self) -> int: - if not self.config.has_section('mqtt'): - return 10 - return self._get_int('mqtt', 'discovery_timeout', 10) - # Optionen - @property - def clear_tables(self) -> bool: - return self._get_bool('options', 'clear_tables', True) - - @property - def log_level(self) -> str: - level = self.config.get('options', 'log_level', fallback='INFO').strip().upper() - valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] - return level if level in valid_levels else 'INFO' - - @property - def log_file(self) -> Optional[str]: - value = self.config.get('options', 'log_file', fallback='').strip() - return value if value else None - - # Advanced - @property - def scan_threads(self) -> int: - if self.config.has_section('advanced'): - return self._get_int('advanced', 'scan_threads', 50) - return 50 - - -# Konfiguration laden (global, wird in main() initialisiert) -config = None - -# Logging konfigurieren (wird nach Config-Laden angepasst) -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - - -class TahomaAPI: - """Klasse für die Kommunikation mit der Tahoma Local API""" - - def __init__(self, gateway_ip: str, api_token: str): - """ - Initialisiert die Tahoma API Verbindung - - Args: - gateway_ip: IP-Adresse der Tahoma Box - api_token: API Token (Bearer Token) - """ - self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI" - self.headers = { - "Authorization": f"Bearer {api_token}", - "Content-Type": "application/json" - } - - def get_setup(self) -> Optional[Dict]: - """ - Ruft die komplette Setup-Konfiguration ab - - Returns: - Dictionary mit allen Geräten oder None bei Fehler - """ - try: - url = f"{self.base_url}/setup" - response = requests.get(url, headers=self.headers, verify=False, timeout=10) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}") - return None - - def get_devices(self) -> List[Dict]: - """ - Extrahiert alle Geräte aus dem Setup - - Returns: - Liste aller Geräte - """ - setup = self.get_setup() - if not setup: - return [] - - devices = setup.get('devices', []) - logger.info(f"{len(devices)} Geräte gefunden") - return devices - - def get_device_definition(self, device_url: str) -> Optional[Dict]: - """ - Ruft die detaillierte Definition eines Geräts ab - - Args: - device_url: URL des Geräts - - Returns: - Dictionary mit Gerätedefinition oder None bei Fehler - """ - try: - # Device URL encodieren - from urllib.parse import quote - encoded_url = quote(device_url, safe='') - url = f"{self.base_url}/setup/devices/{encoded_url}" - response = requests.get(url, headers=self.headers, verify=False, timeout=10) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - logger.debug(f"Fehler beim Abrufen der Device-Definition für {device_url}: {e}") - return None - - def get_device_states(self, device_url: str) -> List[Dict]: - """ - Ruft die aktuellen States eines Geräts ab - - Args: - device_url: URL des Geräts - - Returns: - Liste der States - """ - try: - from urllib.parse import quote - encoded_url = quote(device_url, safe='') - url = f"{self.base_url}/setup/devices/{encoded_url}/states" - response = requests.get(url, headers=self.headers, verify=False, timeout=10) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - logger.debug(f"Fehler beim Abrufen der Device-States für {device_url}: {e}") - return [] - - -class DatabaseManager: - """Klasse für die MySQL-Datenbankoperationen""" - - def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306): - """ - Initialisiert die Datenbankverbindung - - Args: - host: MySQL Host - database: Datenbankname - user: Benutzername - password: Passwort - port: Port (Standard: 3306) - """ - self.host = host - self.database = database - self.user = user - self.password = password - self.port = port - self.connection = None - - def connect(self) -> bool: - """ - Stellt Verbindung zur Datenbank her - - Returns: - True bei Erfolg, False bei Fehler - """ - try: - self.connection = pymysql.connect( - host=self.host, - database=self.database, - user=self.user, - password=self.password, - port=self.port, - charset='utf8mb4' - ) - logger.info("Erfolgreich mit MariaDB/MySQL-Datenbank verbunden") - return True - except Error as e: - logger.error(f"Fehler bei der Datenbankverbindung: {e}") - return False - - def disconnect(self): - """Schließt die Datenbankverbindung""" - if self.connection: - self.connection.close() - logger.info("Datenbankverbindung geschlossen") - - def clear_tables(self): - """Löscht alle Einträge aus allen Tabellen""" - try: - cursor = self.connection.cursor() - # Foreign Key Constraints temporär deaktivieren - cursor.execute("SET FOREIGN_KEY_CHECKS=0") - - # Alle Tabellen leeren - cursor.execute("DELETE FROM command_parameters") - cursor.execute("DELETE FROM actor_commands") - cursor.execute("DELETE FROM actor_states") - cursor.execute("DELETE FROM actors") - - cursor.execute("DELETE FROM sensor_states") - cursor.execute("DELETE FROM sensors") - - # Foreign Key Constraints wieder aktivieren - cursor.execute("SET FOREIGN_KEY_CHECKS=1") - - self.connection.commit() - logger.info("Alle Tabellen geleert") - cursor.close() - except Error as e: - logger.error(f"Fehler beim Leeren der Tabellen: {e}") - self.connection.rollback() - - def insert_actor(self, device_type: str, name: str, url: str, - commands: list, states: list) -> bool: - """ - Fügt einen Aktor mit Commands und States in die Datenbank ein - (AKTUALISIERT für MQTT URL-Support) - """ - try: - cursor = self.connection.cursor() - - # 1. Aktor einfügen - query = """ - INSERT INTO actors (type, name, parameters, url) - VALUES (%s, %s, NULL, %s) - """ - cursor.execute(query, (device_type, name, url)) - actor_id = cursor.lastrowid - - # 2. Commands einfügen - for cmd in commands: - command_name = cmd.get('command', '') - - cmd_query = """ - INSERT INTO actor_commands (actor_id, command_name) - VALUES (%s, %s) - """ - cursor.execute(cmd_query, (actor_id, command_name)) - command_id = cursor.lastrowid - - # Parameter mit URL einfügen - cmd_params = cmd.get('parameters', []) - for param in cmd_params: - param_query = """ - INSERT INTO command_parameters - (command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url) - VALUES (%s, %s, %s, %s, %s, %s, %s) - """ - param_name = param.get('name', '') - param_type = param.get('type', '') - min_val = param.get('min') - max_val = param.get('max') - possible_vals = json.dumps(param.get('values')) if 'values' in param else None - param_url = param.get('url') # NEU: MQTT Topic - - cursor.execute(param_query, - (command_id, param_name, param_type, min_val, max_val, possible_vals, param_url)) - - # 3. States mit URL einfügen - for state in states: - state_query = """ - INSERT INTO actor_states - (actor_id, state_name, state_type, current_value, unit, url) - VALUES (%s, %s, %s, %s, %s, %s) - """ - state_name = state.get('name', '') - state_type = state.get('type', 0) - current_value = str(state.get('current_value', '')) if 'current_value' in state else None - unit = state.get('unit') - state_url = state.get('url') # NEU: MQTT Topic - - cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url)) - - self.connection.commit() - cursor.close() - return True - - except Error as e: - logger.error(f"Fehler beim Einfügen des Aktors {name}: {e}") - self.connection.rollback() - return False - - def insert_sensor(self, device_type: str, name: str, url: str, - states: list) -> bool: - """ - Fügt einen Sensor mit States in die Datenbank ein - (AKTUALISIERT für MQTT URL-Support) - """ - try: - cursor = self.connection.cursor() - - # 1. Sensor einfügen - query = """ - INSERT INTO sensors (type, name, parameters, url) - VALUES (%s, %s, NULL, %s) - """ - cursor.execute(query, (device_type, name, url)) - sensor_id = cursor.lastrowid - - # 2. States mit URL einfügen - for state in states: - state_query = """ - INSERT INTO sensor_states - (sensor_id, state_name, state_type, current_value, unit, url) - VALUES (%s, %s, %s, %s, %s, %s) - """ - state_name = state.get('name', '') - state_type = state.get('type', 0) - current_value = str(state.get('current_value', '')) if 'current_value' in state else None - unit = state.get('unit') - state_url = state.get('url') # NEU: MQTT Topic - - cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url)) - - self.connection.commit() - cursor.close() - return True - - except Error as e: - logger.error(f"Fehler beim Einfügen des Sensors {name}: {e}") - self.connection.rollback() - return False - - -class WLEDDiscovery: - """Klasse für die automatische WLED-Geräteerkennung im Netzwerk""" - - @staticmethod - def discover_devices(timeout: int = 5) -> List[str]: - """ - Sucht nach WLED-Geräten im lokalen Netzwerk mittels mDNS - - Args: - timeout: Timeout in Sekunden für die Suche - - Returns: - Liste mit IP-Adressen gefundener WLED-Geräte - """ - try: - from zeroconf import ServiceBrowser, ServiceListener, Zeroconf - import time - - class WLEDListener(ServiceListener): - def __init__(self): - self.devices = [] - - def add_service(self, zc, type_, name): - info = zc.get_service_info(type_, name) - if info: - # IP-Adresse extrahieren - addresses = [socket.inet_ntoa(addr) for addr in info.addresses] - for addr in addresses: - if addr not in self.devices: - self.devices.append(addr) - logger.info(f"WLED-Gerät gefunden: {name} ({addr})") - - def remove_service(self, zc, type_, name): - pass - - def update_service(self, zc, type_, name): - pass - - zeroconf = Zeroconf() - listener = WLEDListener() - browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener) - - logger.info(f"Suche nach WLED-Geräten (Timeout: {timeout}s)...") - time.sleep(timeout) - - zeroconf.close() - - # Filtern: Nur WLED-Geräte - wled_devices = [] - for ip in listener.devices: - if WLEDDiscovery.is_wled_device(ip): - wled_devices.append(ip) - - logger.info(f"{len(wled_devices)} WLED-Geräte gefunden") - return wled_devices - - except ImportError: - logger.warning("zeroconf-Bibliothek nicht installiert. Verwende Netzwerk-Scan...") - return WLEDDiscovery.scan_network() - except Exception as e: - logger.error(f"Fehler bei WLED-Discovery: {e}") - return [] - - @staticmethod - def scan_network(network: str = None, max_threads: int = 50) -> List[str]: - """ - Scannt das Netzwerk nach WLED-Geräten (Fallback-Methode) - - Args: - network: Netzwerk im Format "192.168.1.0/24" (None = automatisch) - max_threads: Maximale Anzahl paralleler Threads - - Returns: - Liste mit IP-Adressen gefundener WLED-Geräte - """ - import socket - import concurrent.futures - - if network is None: - # Eigene IP ermitteln und Netzwerk ableiten - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - local_ip = s.getsockname()[0] - s.close() - # Netzwerk ableiten (angenommen /24) - network_prefix = '.'.join(local_ip.split('.')[:-1]) - except: - logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x") - network_prefix = "192.168.1" - else: - network_prefix = '.'.join(network.split('.')[:3]) - - logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach WLED-Geräten...") - - def check_ip(ip): - if WLEDDiscovery.is_wled_device(ip): - return ip - return None - - wled_devices = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor: - futures = [executor.submit(check_ip, f"{network_prefix}.{i}") - for i in range(1, 255)] - - for future in concurrent.futures.as_completed(futures): - result = future.result() - if result: - wled_devices.append(result) - logger.info(f"WLED-Gerät gefunden: {result}") - - return wled_devices - - @staticmethod - def is_wled_device(ip: str, timeout: float = 1.0) -> bool: - """ - Prüft ob eine IP-Adresse ein WLED-Gerät ist - - Args: - ip: IP-Adresse - timeout: Timeout für die Anfrage - - Returns: - True wenn WLED-Gerät, False sonst - """ - try: - response = requests.get( - f"http://{ip}/json/info", - timeout=timeout, - headers={'User-Agent': 'TahomaSync/1.0'} - ) - if response.status_code == 200: - data = response.json() - # WLED antwortet mit spezifischen Feldern - return 'ver' in data or 'name' in data - except: - pass - return False - - -class WLEDAPI: - """Klasse für die Kommunikation mit WLED-Geräten""" - - def __init__(self, ip: str): - """ - Initialisiert die WLED-API Verbindung - - Args: - ip: IP-Adresse des WLED-Geräts - """ - self.ip = ip - self.base_url = f"http://{ip}" - - def get_info(self) -> Optional[Dict]: - """ - Ruft Geräteinformationen ab - - Returns: - Dictionary mit Geräteinformationen oder None - """ - try: - response = requests.get(f"{self.base_url}/json/info", timeout=2) - response.raise_for_status() - return response.json() - except Exception as e: - logger.error(f"Fehler beim Abrufen der WLED-Info von {self.ip}: {e}") - return None - - def get_presets(self) -> Optional[Dict]: - """ - Ruft die Presets ab - - Returns: - Dictionary mit Presets oder None - """ - try: - response = requests.get(f"{self.base_url}/presets.json", timeout=2) - response.raise_for_status() - presets_data = response.json() - # Nur die relevanten Daten extrahieren - preset_list = [] - if isinstance(presets_data, dict): - for preset_id, preset_data in presets_data.items(): - preset_name = preset_data.get('n', f'Preset {preset_id}') - preset_list.append({ - int(preset_id): preset_name - }) - - return preset_list - except Exception as e: - logger.error(f"Fehler beim Abrufen der WLED-Presets von {self.ip}: {e}") - return None - - def get_effects(self) -> Optional[Dict]: - """ - Ruft die Effects ab - - Returns: - Dictionary mit effects oder None - """ - try: - response = requests.get(f"{self.base_url}/json/eff", timeout=2) - response.raise_for_status() - eff_data = response.json() - # Nur die relevanten Daten extrahieren - eff_list = [] - for eff_id, eff_data in enumerate(eff_data): - if(eff_data): - eff_name = eff_data - else: - eff_name = "Effect "+eff_id - eff_list.append({ - int(eff_id): eff_name - }) - - return eff_list - except Exception as e: - logger.error(f"Fehler beim Abrufen der WLED-Effects von {self.ip}: {e}") - return None - - def get_state(self) -> Optional[Dict]: - """ - Ruft aktuellen Zustand ab - - Returns: - Dictionary mit aktuellem Zustand oder None - """ - try: - response = requests.get(f"{self.base_url}/json/state", timeout=2) - response.raise_for_status() - return response.json() - except Exception as e: - logger.error(f"Fehler beim Abrufen des WLED-State von {self.ip}: {e}") - return None - - def get_device_data(self) -> Optional[Dict]: - """ - Erstellt ein Geräte-Dictionary im Tahoma-ähnlichen Format - - Returns: - Dictionary mit Gerätedaten - """ - info = self.get_info() - state = self.get_state() - - if not info: - return None - - # Name des Geräts - name = info.get('name', f"WLED {self.ip}") - preset_values = self.get_presets() - eff_values = self.get_effects() - # Commands für WLED erstellen - commands = [ - { - 'command': 'on', - 'parameters': [] - }, - { - 'command': 'off', - 'parameters': [] - }, - { - 'command': 'setBrightness', - 'parameters': [ - { - 'name': 'brightness', - 'type': 'integer', - 'min': 0, - 'max': 255 - } - ] - }, - { - 'command': 'setColor', - 'parameters': [ - { - 'name': 'red', - 'type': 'integer', - 'min': 0, - 'max': 255 - }, - { - 'name': 'green', - 'type': 'integer', - 'min': 0, - 'max': 255 - }, - { - 'name': 'blue', - 'type': 'integer', - 'min': 0, - 'max': 255 - } - ] - }, - { - 'command': 'setEffect', - 'parameters': [ - { - 'name': 'effect', - 'type': 'integer', - 'min': 0, - 'max': 255, - 'values':eff_values - } - ] - }, - { - 'command': 'setPreset', - 'parameters': [ - { - 'name': 'preset', - 'type': 'integer', - 'min': 1, - 'max': 250, - 'values': preset_values - } - ] - } - ] - - # States erstellen - states = [] - if state: - states.append({ - 'name': 'power', - 'type': 'boolean', - 'current_value': state.get('on', False) - }) - states.append({ - 'name': 'brightness', - 'type': 'integer', - 'current_value': state.get('bri', 0) - }) - - # Farbe (erstes Segment) - segments = state.get('seg', []) - if segments and len(segments) > 0: - seg = segments[0] - colors = seg.get('col', [[0,0,0]]) - if colors and len(colors) > 0: - rgb = colors[0] - states.append({ - 'name': 'color_rgb', - 'type': 'array', - 'current_value': rgb - }) - - return { - 'name': name, - 'type': 'WLED', - 'url': f"wled://{self.ip}", - 'commands': commands, - 'states': states, - 'info': { - 'version': info.get('ver', 'unknown'), - 'ip': self.ip, - 'mac': info.get('mac', 'unknown') - } - } - - -class DeviceClassifier: - """Klassifiziert Geräte als Aktoren oder Sensoren""" - - # Bekannte Aktor-Typen (können erweitert werden) - ACTOR_TYPES = { - 'RollerShutter', 'ExteriorScreen', 'Awning', 'Blind', - 'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight', - 'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain', - 'VenetianBlind', 'PergolaScreen' - } - - # Bekannte Sensor-Typen (können erweitert werden) - SENSOR_TYPES = { - 'TemperatureSensor', 'LightSensor', 'HumiditySensor', - 'ContactSensor', 'OccupancySensor', 'SmokeSensor', - 'WaterDetectionSensor', 'WindowHandle', 'MotionSensor', - 'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor' - } - - # Bekannte Kommandos (können erweitert werden) - TAHOMA_COMMANDS = { - # ---------- Jalousien & Rollos ---------- - "setClosure": [ - { - "name": "position", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "0 % = offen, 100 % = zu" - } - ], - "setClosureAndOrientation": [ - { - "name": "position", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "0 % = offen, 100 % = zu" - }, - { - "name": "neigung", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "0 % = offen, 100 % = zu (Neigungswinkel bei Lamellen)" - }, - ], - "setOrientation": [ - { - "name": "neigung", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "0 % = offen, 100 % = zu (Nur Neigungswinkel)" - } - ], - "up": [], # öffnet vollständig → kein Parameter - "down": [], # schließt vollständig → kein Parameter - "my": [], # fährt zur „Meine‑Position“ - "setMyPosition": [ - { - "name": "position", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "Speichert die aktuelle bzw. angegebene Position (0 % = offen, 100 % = zu)" - } - ], - "stop": [], # sofort anhalten - "refresh": [], # Status neu abfragen - - # ---------- Licht / Schalter ---------- - "on": [], - "off": [], - "toggle": [], - "setIntensity": [ - { - "name": "helligkeit", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "Helligkeit des Lichts" - } - ], - "setColor": [ - { - "name": "farbton", - "type": "integer", - "min": 0, - "max": 360, - "unit": "°", - "description": "Farbton (0‑360°)" - }, - { - "name": "sättigung", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "Sättigung des Farbtons" - }, - ], - "setColorTemperature": [ - { - "name": "farbtemperatur", - "type": "integer", - "min": 2000, - "max": 6500, - "unit": "K", - "description": "Farbtemperatur in Kelvin" - } - ], - "setTransition": [ - { - "name": "dauer", - "type": "integer", - "min": 0, - "max": 3600, - "unit": "s", - "description": "Übergangszeit für nachfolgende Befehle" - } - ], - - # ---------- Thermostat ---------- - "setTargetTemperature": [ - { - "name": "temperatur", - "type": "float", - "min": 5.0, - "max": 30.0, - "unit": "°C", - "description": "Soll‑Temperatur" - } - ], - "setMode": [ - { - "name": "betriebsart", - "type": "string", - "enum": ["off", "heating", "cooling", "auto"], - "unit": None, - "description": "Betriebsmodus des Thermostats" - } - ], - "setBoost": [ - { - "name": "boost_dauer", - "type": "integer", - "min": 1, - "max": 180, - "unit": "min", - "description": "Kurz‑Boost‑Dauer" - } - ], - - # ---------- Schalt‑ / Szenen‑Geräte ---------- - "pulse": [ - { - "name": "impuls_dauer", - "type": "integer", - "min": 1, - "max": 3600, - "unit": "s", - "description": "Kurzimpuls‑Dauer" - } - ], - "setLevel": [ - { - "name": "ausgangs_level", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "Ausgangs‑Level (dimmbare Relais)" - } - ], - "trigger": [], # Szene ausführen → kein Parameter - - # ---------- Meta‑Befehle (für alle Geräte) ---------- - "configureReporting": [ - { - "name": "intervall", - "type": "integer", - "min": 30, - "max": 86400, - "unit": "s", - "description": "Meldeintervall für das Gerät" - } - ], - "setBatteryThreshold": [ - { - "name": "warnschwelle", - "type": "integer", - "min": 0, - "max": 100, - "unit": "%", - "description": "Batteriewarnschwelle" - } - ], - } - - - - @classmethod - def is_actor(cls, device: Dict) -> bool: - """ - Prüft, ob ein Gerät ein Aktor ist - - Args: - device: Geräte-Dictionary - - Returns: - True wenn Aktor, False sonst - """ - device_type = device.get('definition', '').get('uiClass', '') - - # Prüfung nach bekannten Typen - if device_type in cls.ACTOR_TYPES: - return True - - # Prüfung nach Commandos (Aktoren haben typischerweise Commands) - commands = device.get('definition', {}).get('commands', []) - if commands and len(commands) > 0: - # Wenn Commands wie open, close, on, off existieren - command_names = [cmd.get('commandName', '') for cmd in commands] - actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'} - if any(cmd in actor_commands for cmd in command_names): - return True - - return False - - @classmethod - def is_sensor(cls, device: Dict) -> bool: - """ - Prüft, ob ein Gerät ein Sensor ist - - Args: - device: Geräte-Dictionary - - Returns: - True wenn Sensor, False sonst - """ - device_type = device.get('definition', '').get('uiClass', '') - controllable_name = device.get('controllableName', '') - - # Prüfung nach bekannten Typen - if device_type in cls.SENSOR_TYPES: - return True - if controllable_name in cls.SENSOR_TYPES: - return True - else: - return False - # Prüfung nach States (Sensoren haben typischerweise nur States, keine Commands) - states = device.get('states', []) - commands = device.get('definition', {}).get('commands', []) - - # Sensor hat States aber keine oder nur wenige Commands - if states and len(states) > 0 and len(commands) <= 1: - return True - - return False - - @classmethod - def extract_actor_data(cls, device: Dict) -> tuple: - """ - Extrahiert Commands und States aus einem Aktor - - Args: - device: Geräte-Dictionary von der Tahoma API - - Returns: - Tuple (commands_list, states_list) - """ - commands = [] - states = [] - - # Commands aus der Definition extrahieren - cmd_definitions = device.get('definition', {}).get('commands', []) - - for cmd in cmd_definitions: - command_name = cmd.get('commandName', '') - - command_entry = { - 'command': command_name, - 'parameters': [] - } - - # HINWEIS: Tahoma API liefert oft keine detaillierten Parameter-Infos - # Daher werden Commands erstmal ohne Parameter-Details gespeichert - #cmd_params = cmd.get('parameters', []) - cmd_params = cls.TAHOMA_COMMANDS.get(command_name,[]) - - for cmd_param in cmd_params: - param_detail = { - 'name': cmd_param.get('name', '') - } - - # Datentyp (falls vorhanden) - param_type = cmd_param.get('type') - if param_type: - param_detail['type'] = param_type - - # Min/Max Werte (meist nicht in Tahoma API vorhanden) - if 'min' in cmd_param: - param_detail['min'] = cmd_param['min'] - if 'max' in cmd_param: - param_detail['max'] = cmd_param['max'] - - # Mögliche Werte (enum) (meist nicht vorhanden) - if 'values' in cmd_param: - param_detail['values'] = cmd_param['values'] - if 'unit' in cmd_param: - param_detail['unit'] = cmd_param['unit'] - if 'description' in cmd_param: - param_detail['description'] = cmd_param['description'] - # Nur hinzufügen wenn Name vorhanden - if param_detail['name']: - command_entry['parameters'].append(param_detail) - - commands.append(command_entry) - - # States extrahieren - state_definitions = device.get('states', []) - for state in state_definitions: - state_name = state.get('name', '') - if state_name: - state_entry = { - 'name': state_name, - 'type': state.get('type', 0) - } - if 'value' in state: - state_entry['current_value'] = state['value'] - states.append(state_entry) - - return commands, states - - @classmethod - def extract_sensor_data(cls, device: Dict) -> list: - """ - Extrahiert States aus einem Sensor - - Args: - device: Geräte-Dictionary von der Tahoma API - - Returns: - Liste der States - """ - states = [] - - # States extrahieren - state_definitions = device.get('states', []) - for state in state_definitions: - state_name = state.get('name', '') - if state_name: - state_entry = { - 'name': state_name, - 'type': state.get('type', 0) - } - - # Aktueller Wert falls vorhanden - if 'value' in state: - state_entry['current_value'] = state['value'] - - states.append(state_entry) - - return states - - -def process_devices(tahoma: TahomaAPI, db: DatabaseManager, - enable_wled: bool = True, wled_timeout: int = 5, - enable_mqtt: bool = True, # NEU - clear_before_insert: bool = True): - """ - Verarbeitet alle Geräte und speichert sie in der Datenbank - - Args: - tahoma: TahomaAPI Instanz - db: DatabaseManager Instanz - enable_wled: WLED-Geräte suchen und hinzufügen - wled_timeout: Timeout für WLED-Discovery in Sekunden - clear_before_insert: Tabellen vor dem Einfügen leeren (Standard: True) - """ - # Optional: Tabellen leeren - if clear_before_insert: - db.clear_tables() - - actor_count = 0 - sensor_count = 0 - unknown_count = 0 - - # ========== TAHOMA-GERÄTE VERARBEITEN ========== - logger.info("=" * 60) - logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN") - logger.info("=" * 60) - - devices = tahoma.get_devices() - - if not devices: - logger.warning("Keine Tahoma-Geräte gefunden") - else: - # Gruppierte Geräte identifizieren (#1, #2, etc.) - device_groups = {} # {base_url: [devices]} - standalone_devices = [] - - for device in devices: - device_url = device.get('deviceURL', '') - - # Prüfen ob URL mit #1, #2, etc. endet - import re - match = re.match(r'(.+)#(\d+)$', device_url) - - if match: - base_url = match.group(1) - if base_url not in device_groups: - device_groups[base_url] = [] - device_groups[base_url].append(device) - else: - standalone_devices.append(device) - - # Gruppierte Geräte verarbeiten - for base_url, group_devices in device_groups.items(): - # Hauptgerät finden (ohne #-Endung oder mit #1) - main_device = None - sub_devices = [] - - for dev in group_devices: - url = dev.get('deviceURL', '') - if url.endswith('#1'): - main_device = dev - else: - sub_devices.append(dev) - - # Falls kein #1, nehme das erste Gerät als Hauptgerät - if not main_device and group_devices: - main_device = group_devices[0] - sub_devices = group_devices[1:] - - # Name vom Hauptgerät für alle übernehmen - main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt' - - # Alle Geräte der Gruppe verarbeiten mit gemeinsamem Namen - for device in group_devices: - device_url = device.get('deviceURL', '') - # controllableName als Typ verwenden - device_type = device.get('controllableName', device.get('uiClass', 'Unknown')) - - # Gerät klassifizieren - is_actor = DeviceClassifier.is_actor(device) - is_sensor = DeviceClassifier.is_sensor(device) - - if is_actor: - commands, states = DeviceClassifier.extract_actor_data(device) - if db.insert_actor(device_type, main_name, device_url, commands, states): - actor_count += 1 - logger.info(f"✓ Aktor [Gruppe]: {main_name} ({device_type}) - " - f"{len(commands)} Commands, {len(states)} States") - - elif is_sensor: - states = DeviceClassifier.extract_sensor_data(device) - if db.insert_sensor(device_type, main_name, device_url, states): - sensor_count += 1 - logger.info(f"✓ Sensor [Gruppe]: {main_name} ({device_type}) - " - f"{len(states)} States") - else: - unknown_count += 1 - logger.warning(f"⚠ Unbekannt [Gruppe]: {main_name} ({device_type}-{device_url})") - - # Standalone-Geräte verarbeiten - for device in standalone_devices: - device_url = device.get('deviceURL', '') - device_name = device.get('label', 'Unbekannt') - # controllableName als Typ verwenden - device_type = device.get('controllableName', device.get('uiClass', 'Unknown')) - - # Gerät klassifizieren - is_actor = DeviceClassifier.is_actor(device) - is_sensor = DeviceClassifier.is_sensor(device) - - if is_actor: - # Daten extrahieren - commands, states = DeviceClassifier.extract_actor_data(device) - - # In Datenbank speichern - if db.insert_actor(device_type, device_name, device_url, commands, states): - actor_count += 1 - logger.info(f"✓ Aktor: {device_name} ({device_type}) - " - f"{len(commands)} Commands, {len(states)} States") - - elif is_sensor: - # Daten extrahieren - states = DeviceClassifier.extract_sensor_data(device) - - # In Datenbank speichern - if db.insert_sensor(device_type, device_name, device_url, states): - sensor_count += 1 - logger.info(f"✓ Sensor: {device_name} ({device_type}) - " - f"{len(states)} States") - - else: - unknown_count += 1 - logger.warning(f"⚠ Unbekannt: {device_name} ({device_type})") - - # ========== WLED-GERÄTE SUCHEN UND VERARBEITEN ========== - if enable_wled: - logger.info("\n" + "=" * 60) - logger.info("WLED-GERÄTE WERDEN GESUCHT") - logger.info("=" * 60) - - wled_ips = WLEDDiscovery.discover_devices(timeout=wled_timeout) - - # Manuelle IPs aus Config hinzufügen - if config and config.wled_manual_ips: - logger.info(f"Füge {len(config.wled_manual_ips)} manuelle WLED-IPs hinzu...") - for manual_ip in config.wled_manual_ips: - if manual_ip not in wled_ips: - if WLEDDiscovery.is_wled_device(manual_ip): - wled_ips.append(manual_ip) - logger.info(f"✓ Manuelles WLED-Gerät: {manual_ip}") - else: - logger.warning(f"⚠ {manual_ip} ist kein WLED-Gerät") - - if not wled_ips: - logger.info("Keine WLED-Geräte gefunden") - else: - logger.info(f"\n{len(wled_ips)} WLED-Geräte gefunden, füge sie hinzu...") - - for ip in wled_ips: - try: - wled = WLEDAPI(ip) - device_data = wled.get_device_data() - - if device_data: - name = device_data['name'] - device_type = device_data['type'] - url = device_data['url'] - commands = device_data['commands'] - states = device_data['states'] - - # WLED immer als Aktor hinzufügen - if db.insert_actor(device_type, name, url, commands, states): - actor_count += 1 - logger.info(f"✓ WLED: {name} ({ip}) - " - f"{len(commands)} Commands, {len(states)} States") - else: - logger.warning(f"⚠ Konnte keine Daten von WLED {ip} abrufen") - - except Exception as e: - logger.error(f"✗ Fehler beim Verarbeiten von WLED {ip}: {e}") - # ========== MQTT-GERÄTE SUCHEN UND VERARBEITEN ========== - # ========== MQTT-GERÄTE SUCHEN UND VERARBEITEN ========== - if enable_mqtt: - logger.info("\n" + "=" * 60) - logger.info("MQTT/HOME ASSISTANT DISCOVERY") - logger.info("=" * 60) - - try: - # MQTT Discovery initialisieren - mqtt_discovery = HomeAssistantDiscovery( - broker=config.mqtt_broker, - port=config.mqtt_port, - username=config.mqtt_username, - password=config.mqtt_password, - discovery_prefix=config.mqtt_discovery_prefix - ) - - # Verbinden - if mqtt_discovery.connect(): - # Discovery durchführen - mqtt_entities = mqtt_discovery.discover_devices( - timeout=config.mqtt_discovery_timeout - ) - - # Entities nach Geräten gruppieren - mqtt_devices = MQTTDeviceConverter.group_entities_by_device(mqtt_entities) - - # Geräte verarbeiten - if not mqtt_devices: - logger.info("Keine MQTT-Geräte gefunden") - else: - logger.info(f"\n{len(mqtt_devices)} MQTT-Geräte gefunden (aus {len(mqtt_entities)} Entities)") - - for device_id, device_data in mqtt_devices.items(): - try: - # Gerät in Actor/Sensor konvertieren - actor_data, sensor_data = MQTTDeviceConverter.convert_device_to_actors_and_sensors( - device_id, device_data - ) - - # Actor speichern falls vorhanden - if actor_data: - if db.insert_actor( - actor_data['type'], - actor_data['name'], - actor_data['url'], - actor_data['commands'], - actor_data['states'] - ): - actor_count += 1 - logger.info(f"✓ MQTT Actor: {actor_data['name']} - " - f"{len(actor_data['commands'])} Commands, " - f"{len(actor_data['states'])} States") - - # Sensor speichern falls vorhanden - if sensor_data: - if db.insert_sensor( - sensor_data['type'], - sensor_data['name'], - sensor_data['url'], - sensor_data['states'] - ): - sensor_count += 1 - logger.info(f"✓ MQTT Sensor: {sensor_data['name']} - " - f"{len(sensor_data['states'])} States") - - except Exception as e: - logger.error(f"✗ Fehler beim Verarbeiten von MQTT-Gerät {device_id}: {e}") - import traceback - traceback.print_exc() - - # Verbindung trennen - mqtt_discovery.disconnect() - else: - logger.error("MQTT-Verbindung fehlgeschlagen") - - except Exception as e: - logger.error(f"✗ MQTT Discovery Fehler: {e}") - import traceback - traceback.print_exc() - - - # ========== ZUSAMMENFASSUNG ========== - logger.info("\n" + "=" * 60) - logger.info("ZUSAMMENFASSUNG") - logger.info("=" * 60) - logger.info(f"Aktoren gespeichert: {actor_count}") - logger.info(f"Sensoren gespeichert: {sensor_count}") - logger.info(f"Unbekannte Geräte: {unknown_count}") - logger.info("=" * 60) - - -def main(): - """Hauptfunktion""" - global config - - # Konfigurationsdatei laden - try: - config = Config('config.ini') - logger.info("Konfiguration erfolgreich geladen") - except FileNotFoundError as e: - print(f"FEHLER: {e}") - print("\nBitte erstellen Sie eine config.ini Datei mit Ihren Einstellungen.") - print("Siehe config.ini Vorlage für Details.") - return - except Exception as e: - print(f"FEHLER beim Laden der Konfiguration: {e}") - return - - # Logging-Level anpassen - log_level = getattr(logging, config.log_level) - logger.setLevel(log_level) - - # Optional: Log-Datei einrichten - if config.log_file: - file_handler = logging.FileHandler(config.log_file, encoding='utf-8') - file_handler.setLevel(log_level) - file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) - logger.addHandler(file_handler) - logger.info(f"Logging in Datei: {config.log_file}") - - # Tahoma API initialisieren - logger.info("Verbinde mit Tahoma Box...") - tahoma = TahomaAPI(config.tahoma_ip, config.tahoma_token) - - # Datenbank initialisieren - logger.info("Verbinde mit MySQL-Datenbank...") - db = DatabaseManager( - config.db_host, - config.db_name, - config.db_user, - config.db_password, - config.db_port - ) - - if not db.connect(): - logger.error("Datenbankverbindung fehlgeschlagen. Abbruch.") - return - - try: - # Geräte verarbeiten und in Datenbank speichern - process_devices( - tahoma, - db, - enable_wled=config.wled_enable, - wled_timeout=config.wled_discovery_timeout, - enable_mqtt=config.mqtt_enable, # NEU - clear_before_insert=config.clear_tables - ) - logger.info("\n✓ Import erfolgreich abgeschlossen!") - - except Exception as e: - logger.error(f"✗ Fehler während der Verarbeitung: {e}") - import traceback - traceback.print_exc() - - finally: - # Datenbankverbindung schließen - db.disconnect() - - -if __name__ == "__main__": +#!/usr/bin/env python3 +""" +Somfy Tahoma Local API to MySQL Database Script +Liest alle Aktoren und Sensoren aus der Tahoma Box und speichert sie in MySQL +""" + +import requests +import pymysql +from pymysql import Error +import json +import logging +from typing import List, Dict, Optional +import urllib3 +import socket +import configparser +import os +from mqtt_discovery import HomeAssistantDiscovery, MQTTDeviceConverter + +# SSL-Warnungen deaktivieren (Tahoma verwendet selbst-signierte Zertifikate) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +class Config: + """Lädt und verwaltet die Konfiguration aus config.ini""" + + def __init__(self, config_file: str = 'config.ini'): + """ + Lädt die Konfiguration + + Args: + config_file: Name der Konfigurationsdatei (wird im Script-Verzeichnis gesucht) + """ + # Verzeichnis des Scripts ermitteln + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, config_file) + + if not os.path.exists(config_path): + raise FileNotFoundError( + f"Konfigurationsdatei '{config_file}' nicht gefunden!\n" + f"Erwartet in: {script_dir}\n" + f"Bitte erstellen Sie die Datei anhand der Vorlage config.ini.example" + ) + + self.config = configparser.ConfigParser() + self.config.read(config_path, encoding='utf-8') + + logger.info(f"Konfiguration geladen von: {config_path}") + + # Konfiguration validieren + self._validate() + + def _validate(self): + """Validiert die Konfiguration""" + required_sections = ['tahoma', 'database', 'wled', 'options'] + for section in required_sections: + if not self.config.has_section(section): + raise ValueError(f"Erforderliche Sektion '[{section}]' fehlt in config.ini") + + def _get_bool(self, section: str, key: str, fallback: bool = False) -> bool: + """Hilfsmethode zum Lesen von Boolean-Werten""" + value = self.config.get(section, key, fallback=str(fallback)).strip().lower() + return value in ('true', '1', 'yes', 'on') + + def _get_int(self, section: str, key: str, fallback: int = 0) -> int: + """Hilfsmethode zum Lesen von Integer-Werten""" + try: + return self.config.getint(section, key, fallback=fallback) + except ValueError: + return fallback + + def _get_list(self, section: str, key: str) -> List[str]: + """Hilfsmethode zum Lesen von Listen (komma-getrennt)""" + value = self.config.get(section, key, fallback='').strip() + if not value: + return [] + return [item.strip() for item in value.split(',') if item.strip()] + + # Tahoma Konfiguration + @property + def tahoma_ip(self) -> str: + return self.config.get('tahoma', 'ip').strip() + + @property + def tahoma_token(self) -> str: + return self.config.get('tahoma', 'token').strip() + + @property + def tahoma_timeout(self) -> int: + if self.config.has_section('advanced'): + return self._get_int('advanced', 'tahoma_timeout', 10) + return 10 + + # Datenbank Konfiguration + @property + def db_host(self) -> str: + return self.config.get('database', 'host').strip() + + @property + def db_port(self) -> int: + return self._get_int('database', 'port', 3306) + + @property + def db_name(self) -> str: + return self.config.get('database', 'database').strip() + + @property + def db_user(self) -> str: + return self.config.get('database', 'user').strip() + + @property + def db_password(self) -> str: + return self.config.get('database', 'password').strip() + + # WLED Konfiguration + @property + def wled_enable(self) -> bool: + return self._get_bool('wled', 'enable', True) + + @property + def wled_discovery_timeout(self) -> int: + return self._get_int('wled', 'discovery_timeout', 5) + + @property + def wled_manual_ips(self) -> List[str]: + return self._get_list('wled', 'manual_ips') + + @property + def wled_scan_network(self) -> Optional[str]: + value = self.config.get('wled', 'scan_network', fallback='').strip() + return value if value else None + + @property + def wled_timeout(self) -> int: + if self.config.has_section('advanced'): + return self._get_int('advanced', 'wled_timeout', 2) + return 2 + # MQTT Konfiguration + @property + def mqtt_enable(self) -> bool: + if not self.config.has_section('mqtt'): + return False + return self._get_bool('mqtt', 'enable', False) + + @property + def mqtt_broker(self) -> str: + if not self.config.has_section('mqtt'): + return 'localhost' + return self.config.get('mqtt', 'broker', fallback='localhost').strip() + + @property + def mqtt_port(self) -> int: + if not self.config.has_section('mqtt'): + return 1883 + return self._get_int('mqtt', 'port', 1883) + + @property + def mqtt_username(self) -> Optional[str]: + if not self.config.has_section('mqtt'): + return None + value = self.config.get('mqtt', 'username', fallback='').strip() + return value if value else None + + @property + def mqtt_password(self) -> Optional[str]: + if not self.config.has_section('mqtt'): + return None + value = self.config.get('mqtt', 'password', fallback='').strip() + return value if value else None + + @property + def mqtt_discovery_prefix(self) -> str: + if not self.config.has_section('mqtt'): + return 'homeassistant' + return self.config.get('mqtt', 'discovery_prefix', fallback='homeassistant').strip() + + @property + def mqtt_discovery_timeout(self) -> int: + if not self.config.has_section('mqtt'): + return 10 + return self._get_int('mqtt', 'discovery_timeout', 10) + # Optionen + @property + def clear_tables(self) -> bool: + return self._get_bool('options', 'clear_tables', True) + + @property + def log_level(self) -> str: + level = self.config.get('options', 'log_level', fallback='INFO').strip().upper() + valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + return level if level in valid_levels else 'INFO' + + @property + def log_file(self) -> Optional[str]: + value = self.config.get('options', 'log_file', fallback='').strip() + return value if value else None + + # Advanced + @property + def scan_threads(self) -> int: + if self.config.has_section('advanced'): + return self._get_int('advanced', 'scan_threads', 50) + return 50 + + +# Konfiguration laden (global, wird in main() initialisiert) +config = None + +# Logging konfigurieren (wird nach Config-Laden angepasst) +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class TahomaAPI: + """Klasse für die Kommunikation mit der Tahoma Local API""" + + def __init__(self, gateway_ip: str, api_token: str): + """ + Initialisiert die Tahoma API Verbindung + + Args: + gateway_ip: IP-Adresse der Tahoma Box + api_token: API Token (Bearer Token) + """ + self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI" + self.headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + + def get_setup(self) -> Optional[Dict]: + """ + Ruft die komplette Setup-Konfiguration ab + + Returns: + Dictionary mit allen Geräten oder None bei Fehler + """ + try: + url = f"{self.base_url}/setup" + response = requests.get(url, headers=self.headers, verify=False, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}") + return None + + def get_devices(self) -> List[Dict]: + """ + Extrahiert alle Geräte aus dem Setup + + Returns: + Liste aller Geräte + """ + setup = self.get_setup() + if not setup: + return [] + + devices = setup.get('devices', []) + logger.info(f"{len(devices)} Geräte gefunden") + return devices + + def get_device_definition(self, device_url: str) -> Optional[Dict]: + """ + Ruft die detaillierte Definition eines Geräts ab + + Args: + device_url: URL des Geräts + + Returns: + Dictionary mit Gerätedefinition oder None bei Fehler + """ + try: + # Device URL encodieren + from urllib.parse import quote + encoded_url = quote(device_url, safe='') + url = f"{self.base_url}/setup/devices/{encoded_url}" + response = requests.get(url, headers=self.headers, verify=False, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logger.debug(f"Fehler beim Abrufen der Device-Definition für {device_url}: {e}") + return None + + def get_device_states(self, device_url: str) -> List[Dict]: + """ + Ruft die aktuellen States eines Geräts ab + + Args: + device_url: URL des Geräts + + Returns: + Liste der States + """ + try: + from urllib.parse import quote + encoded_url = quote(device_url, safe='') + url = f"{self.base_url}/setup/devices/{encoded_url}/states" + response = requests.get(url, headers=self.headers, verify=False, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logger.debug(f"Fehler beim Abrufen der Device-States für {device_url}: {e}") + return [] + + +class DatabaseManager: + """Klasse für die MySQL-Datenbankoperationen""" + + def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306): + """ + Initialisiert die Datenbankverbindung + + Args: + host: MySQL Host + database: Datenbankname + user: Benutzername + password: Passwort + port: Port (Standard: 3306) + """ + self.host = host + self.database = database + self.user = user + self.password = password + self.port = port + self.connection = None + + def connect(self) -> bool: + """ + Stellt Verbindung zur Datenbank her + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + self.connection = pymysql.connect( + host=self.host, + database=self.database, + user=self.user, + password=self.password, + port=self.port, + charset='utf8mb4' + ) + logger.info("Erfolgreich mit MariaDB/MySQL-Datenbank verbunden") + return True + except Error as e: + logger.error(f"Fehler bei der Datenbankverbindung: {e}") + return False + + def disconnect(self): + """Schließt die Datenbankverbindung""" + if self.connection: + self.connection.close() + logger.info("Datenbankverbindung geschlossen") + + def clear_tables(self): + """Löscht alle Einträge aus allen Tabellen""" + try: + cursor = self.connection.cursor() + # Foreign Key Constraints temporär deaktivieren + cursor.execute("SET FOREIGN_KEY_CHECKS=0") + + # Alle Tabellen leeren + cursor.execute("DELETE FROM command_parameters") + cursor.execute("DELETE FROM actor_commands") + cursor.execute("DELETE FROM actor_states") + cursor.execute("DELETE FROM actors") + + cursor.execute("DELETE FROM sensor_states") + cursor.execute("DELETE FROM sensors") + + # Foreign Key Constraints wieder aktivieren + cursor.execute("SET FOREIGN_KEY_CHECKS=1") + + self.connection.commit() + logger.info("Alle Tabellen geleert") + cursor.close() + except Error as e: + logger.error(f"Fehler beim Leeren der Tabellen: {e}") + self.connection.rollback() + + def insert_actor(self, device_type: str, name: str, url: str, + commands: list, states: list) -> bool: + """ + Fügt einen Aktor mit Commands und States in die Datenbank ein + (AKTUALISIERT für MQTT URL-Support) + """ + try: + cursor = self.connection.cursor() + + # 1. Aktor einfügen + query = """ + INSERT INTO actors (type, name, parameters, url) + VALUES (%s, %s, NULL, %s) + """ + cursor.execute(query, (device_type, name, url)) + actor_id = cursor.lastrowid + + # 2. Commands einfügen + for cmd in commands: + command_name = cmd.get('command', '') + + cmd_query = """ + INSERT INTO actor_commands (actor_id, command_name) + VALUES (%s, %s) + """ + cursor.execute(cmd_query, (actor_id, command_name)) + command_id = cursor.lastrowid + + # Parameter mit URL einfügen + cmd_params = cmd.get('parameters', []) + for param in cmd_params: + param_query = """ + INSERT INTO command_parameters + (command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """ + param_name = param.get('name', '') + param_type = param.get('type', '') + min_val = param.get('min') + max_val = param.get('max') + possible_vals = json.dumps(param.get('values')) if 'values' in param else None + param_url = param.get('url') # NEU: MQTT Topic + + cursor.execute(param_query, + (command_id, param_name, param_type, min_val, max_val, possible_vals, param_url)) + + # 3. States mit URL einfügen + for state in states: + state_query = """ + INSERT INTO actor_states + (actor_id, state_name, state_type, current_value, unit, url) + VALUES (%s, %s, %s, %s, %s, %s) + """ + state_name = state.get('name', '') + state_type = state.get('type', 0) + current_value = str(state.get('current_value', '')) if 'current_value' in state else None + unit = state.get('unit') + state_url = state.get('url') # NEU: MQTT Topic + + cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url)) + + self.connection.commit() + cursor.close() + return True + + except Error as e: + logger.error(f"Fehler beim Einfügen des Aktors {name}: {e}") + self.connection.rollback() + return False + + def insert_sensor(self, device_type: str, name: str, url: str, + states: list) -> bool: + """ + Fügt einen Sensor mit States in die Datenbank ein + (AKTUALISIERT für MQTT URL-Support) + """ + try: + cursor = self.connection.cursor() + + # 1. Sensor einfügen + query = """ + INSERT INTO sensors (type, name, parameters, url) + VALUES (%s, %s, NULL, %s) + """ + cursor.execute(query, (device_type, name, url)) + sensor_id = cursor.lastrowid + + # 2. States mit URL einfügen + for state in states: + state_query = """ + INSERT INTO sensor_states + (sensor_id, state_name, state_type, current_value, unit, url) + VALUES (%s, %s, %s, %s, %s, %s) + """ + state_name = state.get('name', '') + state_type = state.get('type', 0) + current_value = str(state.get('current_value', '')) if 'current_value' in state else None + unit = state.get('unit') + state_url = state.get('url') # NEU: MQTT Topic + + cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url)) + + self.connection.commit() + cursor.close() + return True + + except Error as e: + logger.error(f"Fehler beim Einfügen des Sensors {name}: {e}") + self.connection.rollback() + return False + + +class WLEDDiscovery: + """Klasse für die automatische WLED-Geräteerkennung im Netzwerk""" + + @staticmethod + def discover_devices(timeout: int = 5) -> List[str]: + """ + Sucht nach WLED-Geräten im lokalen Netzwerk mittels mDNS + + Args: + timeout: Timeout in Sekunden für die Suche + + Returns: + Liste mit IP-Adressen gefundener WLED-Geräte + """ + try: + from zeroconf import ServiceBrowser, ServiceListener, Zeroconf + import time + + class WLEDListener(ServiceListener): + def __init__(self): + self.devices = [] + + def add_service(self, zc, type_, name): + info = zc.get_service_info(type_, name) + if info: + # IP-Adresse extrahieren + addresses = [socket.inet_ntoa(addr) for addr in info.addresses] + for addr in addresses: + if addr not in self.devices: + self.devices.append(addr) + logger.info(f"WLED-Gerät gefunden: {name} ({addr})") + + def remove_service(self, zc, type_, name): + pass + + def update_service(self, zc, type_, name): + pass + + zeroconf = Zeroconf() + listener = WLEDListener() + browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener) + + logger.info(f"Suche nach WLED-Geräten (Timeout: {timeout}s)...") + time.sleep(timeout) + + zeroconf.close() + + # Filtern: Nur WLED-Geräte + wled_devices = [] + for ip in listener.devices: + if WLEDDiscovery.is_wled_device(ip): + wled_devices.append(ip) + + logger.info(f"{len(wled_devices)} WLED-Geräte gefunden") + return wled_devices + + except ImportError: + logger.warning("zeroconf-Bibliothek nicht installiert. Verwende Netzwerk-Scan...") + return WLEDDiscovery.scan_network() + except Exception as e: + logger.error(f"Fehler bei WLED-Discovery: {e}") + return [] + + @staticmethod + def scan_network(network: str = None, max_threads: int = 50) -> List[str]: + """ + Scannt das Netzwerk nach WLED-Geräten (Fallback-Methode) + + Args: + network: Netzwerk im Format "192.168.1.0/24" (None = automatisch) + max_threads: Maximale Anzahl paralleler Threads + + Returns: + Liste mit IP-Adressen gefundener WLED-Geräte + """ + import socket + import concurrent.futures + + if network is None: + # Eigene IP ermitteln und Netzwerk ableiten + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + s.close() + # Netzwerk ableiten (angenommen /24) + network_prefix = '.'.join(local_ip.split('.')[:-1]) + except: + logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x") + network_prefix = "192.168.1" + else: + network_prefix = '.'.join(network.split('.')[:3]) + + logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach WLED-Geräten...") + + def check_ip(ip): + if WLEDDiscovery.is_wled_device(ip): + return ip + return None + + wled_devices = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor: + futures = [executor.submit(check_ip, f"{network_prefix}.{i}") + for i in range(1, 255)] + + for future in concurrent.futures.as_completed(futures): + result = future.result() + if result: + wled_devices.append(result) + logger.info(f"WLED-Gerät gefunden: {result}") + + return wled_devices + + @staticmethod + def is_wled_device(ip: str, timeout: float = 1.0) -> bool: + """ + Prüft ob eine IP-Adresse ein WLED-Gerät ist + + Args: + ip: IP-Adresse + timeout: Timeout für die Anfrage + + Returns: + True wenn WLED-Gerät, False sonst + """ + try: + response = requests.get( + f"http://{ip}/json/info", + timeout=timeout, + headers={'User-Agent': 'TahomaSync/1.0'} + ) + if response.status_code == 200: + data = response.json() + # WLED antwortet mit spezifischen Feldern + return 'ver' in data or 'name' in data + except: + pass + return False + + +class WLEDAPI: + """Klasse für die Kommunikation mit WLED-Geräten""" + + def __init__(self, ip: str): + """ + Initialisiert die WLED-API Verbindung + + Args: + ip: IP-Adresse des WLED-Geräts + """ + self.ip = ip + self.base_url = f"http://{ip}" + + def get_info(self) -> Optional[Dict]: + """ + Ruft Geräteinformationen ab + + Returns: + Dictionary mit Geräteinformationen oder None + """ + try: + response = requests.get(f"{self.base_url}/json/info", timeout=2) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Fehler beim Abrufen der WLED-Info von {self.ip}: {e}") + return None + + def get_presets(self) -> Optional[Dict]: + """ + Ruft die Presets ab + + Returns: + Dictionary mit Presets oder None + """ + try: + response = requests.get(f"{self.base_url}/presets.json", timeout=2) + response.raise_for_status() + presets_data = response.json() + # Nur die relevanten Daten extrahieren + preset_list = [] + if isinstance(presets_data, dict): + for preset_id, preset_data in presets_data.items(): + preset_name = preset_data.get('n', f'Preset {preset_id}') + preset_list.append({ + int(preset_id): preset_name + }) + + return preset_list + except Exception as e: + logger.error(f"Fehler beim Abrufen der WLED-Presets von {self.ip}: {e}") + return None + + def get_effects(self) -> Optional[Dict]: + """ + Ruft die Effects ab + + Returns: + Dictionary mit effects oder None + """ + try: + response = requests.get(f"{self.base_url}/json/eff", timeout=2) + response.raise_for_status() + eff_data = response.json() + # Nur die relevanten Daten extrahieren + eff_list = [] + for eff_id, eff_data in enumerate(eff_data): + if(eff_data): + eff_name = eff_data + else: + eff_name = "Effect "+eff_id + eff_list.append({ + int(eff_id): eff_name + }) + + return eff_list + except Exception as e: + logger.error(f"Fehler beim Abrufen der WLED-Effects von {self.ip}: {e}") + return None + + def get_state(self) -> Optional[Dict]: + """ + Ruft aktuellen Zustand ab + + Returns: + Dictionary mit aktuellem Zustand oder None + """ + try: + response = requests.get(f"{self.base_url}/json/state", timeout=2) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Fehler beim Abrufen des WLED-State von {self.ip}: {e}") + return None + + def get_device_data(self) -> Optional[Dict]: + """ + Erstellt ein Geräte-Dictionary im Tahoma-ähnlichen Format + + Returns: + Dictionary mit Gerätedaten + """ + info = self.get_info() + state = self.get_state() + + if not info: + return None + + # Name des Geräts + name = info.get('name', f"WLED {self.ip}") + preset_values = self.get_presets() + eff_values = self.get_effects() + # Commands für WLED erstellen + commands = [ + { + 'command': 'on', + 'parameters': [] + }, + { + 'command': 'off', + 'parameters': [] + }, + { + 'command': 'setBrightness', + 'parameters': [ + { + 'name': 'brightness', + 'type': 'integer', + 'min': 0, + 'max': 255 + } + ] + }, + { + 'command': 'setColor', + 'parameters': [ + { + 'name': 'red', + 'type': 'integer', + 'min': 0, + 'max': 255 + }, + { + 'name': 'green', + 'type': 'integer', + 'min': 0, + 'max': 255 + }, + { + 'name': 'blue', + 'type': 'integer', + 'min': 0, + 'max': 255 + } + ] + }, + { + 'command': 'setEffect', + 'parameters': [ + { + 'name': 'effect', + 'type': 'integer', + 'min': 0, + 'max': 255, + 'values':eff_values + } + ] + }, + { + 'command': 'setPreset', + 'parameters': [ + { + 'name': 'preset', + 'type': 'integer', + 'min': 1, + 'max': 250, + 'values': preset_values + } + ] + } + ] + + # States erstellen + states = [] + if state: + states.append({ + 'name': 'power', + 'type': 'boolean', + 'current_value': state.get('on', False) + }) + states.append({ + 'name': 'brightness', + 'type': 'integer', + 'current_value': state.get('bri', 0) + }) + + # Farbe (erstes Segment) + segments = state.get('seg', []) + if segments and len(segments) > 0: + seg = segments[0] + colors = seg.get('col', [[0,0,0]]) + if colors and len(colors) > 0: + rgb = colors[0] + states.append({ + 'name': 'color_rgb', + 'type': 'array', + 'current_value': rgb + }) + + return { + 'name': name, + 'type': 'WLED', + 'url': f"wled://{self.ip}", + 'commands': commands, + 'states': states, + 'info': { + 'version': info.get('ver', 'unknown'), + 'ip': self.ip, + 'mac': info.get('mac', 'unknown') + } + } + + +class DeviceClassifier: + """Klassifiziert Geräte als Aktoren oder Sensoren""" + + # Bekannte Aktor-Typen (können erweitert werden) + ACTOR_TYPES = { + 'RollerShutter', 'ExteriorScreen', 'Awning', 'Blind', + 'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight', + 'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain', + 'VenetianBlind', 'PergolaScreen' + } + + # Bekannte Sensor-Typen (können erweitert werden) + SENSOR_TYPES = { + 'TemperatureSensor', 'LightSensor', 'HumiditySensor', + 'ContactSensor', 'OccupancySensor', 'SmokeSensor', + 'WaterDetectionSensor', 'WindowHandle', 'MotionSensor', + 'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor' + } + + # Bekannte Kommandos (können erweitert werden) + TAHOMA_COMMANDS = { + # ---------- Jalousien & Rollos ---------- + "setClosure": [ + { + "name": "position", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "0 % = offen, 100 % = zu" + } + ], + "setClosureAndOrientation": [ + { + "name": "position", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "0 % = offen, 100 % = zu" + }, + { + "name": "neigung", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "0 % = offen, 100 % = zu (Neigungswinkel bei Lamellen)" + }, + ], + "setOrientation": [ + { + "name": "neigung", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "0 % = offen, 100 % = zu (Nur Neigungswinkel)" + } + ], + "up": [], # öffnet vollständig → kein Parameter + "down": [], # schließt vollständig → kein Parameter + "my": [], # fährt zur „Meine‑Position“ + "setMyPosition": [ + { + "name": "position", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "Speichert die aktuelle bzw. angegebene Position (0 % = offen, 100 % = zu)" + } + ], + "stop": [], # sofort anhalten + "refresh": [], # Status neu abfragen + + # ---------- Licht / Schalter ---------- + "on": [], + "off": [], + "toggle": [], + "setIntensity": [ + { + "name": "helligkeit", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "Helligkeit des Lichts" + } + ], + "setColor": [ + { + "name": "farbton", + "type": "integer", + "min": 0, + "max": 360, + "unit": "°", + "description": "Farbton (0‑360°)" + }, + { + "name": "sättigung", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "Sättigung des Farbtons" + }, + ], + "setColorTemperature": [ + { + "name": "farbtemperatur", + "type": "integer", + "min": 2000, + "max": 6500, + "unit": "K", + "description": "Farbtemperatur in Kelvin" + } + ], + "setTransition": [ + { + "name": "dauer", + "type": "integer", + "min": 0, + "max": 3600, + "unit": "s", + "description": "Übergangszeit für nachfolgende Befehle" + } + ], + + # ---------- Thermostat ---------- + "setTargetTemperature": [ + { + "name": "temperatur", + "type": "float", + "min": 5.0, + "max": 30.0, + "unit": "°C", + "description": "Soll‑Temperatur" + } + ], + "setMode": [ + { + "name": "betriebsart", + "type": "string", + "enum": ["off", "heating", "cooling", "auto"], + "unit": None, + "description": "Betriebsmodus des Thermostats" + } + ], + "setBoost": [ + { + "name": "boost_dauer", + "type": "integer", + "min": 1, + "max": 180, + "unit": "min", + "description": "Kurz‑Boost‑Dauer" + } + ], + + # ---------- Schalt‑ / Szenen‑Geräte ---------- + "pulse": [ + { + "name": "impuls_dauer", + "type": "integer", + "min": 1, + "max": 3600, + "unit": "s", + "description": "Kurzimpuls‑Dauer" + } + ], + "setLevel": [ + { + "name": "ausgangs_level", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "Ausgangs‑Level (dimmbare Relais)" + } + ], + "trigger": [], # Szene ausführen → kein Parameter + + # ---------- Meta‑Befehle (für alle Geräte) ---------- + "configureReporting": [ + { + "name": "intervall", + "type": "integer", + "min": 30, + "max": 86400, + "unit": "s", + "description": "Meldeintervall für das Gerät" + } + ], + "setBatteryThreshold": [ + { + "name": "warnschwelle", + "type": "integer", + "min": 0, + "max": 100, + "unit": "%", + "description": "Batteriewarnschwelle" + } + ], + } + + + + @classmethod + def is_actor(cls, device: Dict) -> bool: + """ + Prüft, ob ein Gerät ein Aktor ist + + Args: + device: Geräte-Dictionary + + Returns: + True wenn Aktor, False sonst + """ + device_type = device.get('definition', '').get('uiClass', '') + + # Prüfung nach bekannten Typen + if device_type in cls.ACTOR_TYPES: + return True + + # Prüfung nach Commandos (Aktoren haben typischerweise Commands) + commands = device.get('definition', {}).get('commands', []) + if commands and len(commands) > 0: + # Wenn Commands wie open, close, on, off existieren + command_names = [cmd.get('commandName', '') for cmd in commands] + actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'} + if any(cmd in actor_commands for cmd in command_names): + return True + + return False + + @classmethod + def is_sensor(cls, device: Dict) -> bool: + """ + Prüft, ob ein Gerät ein Sensor ist + + Args: + device: Geräte-Dictionary + + Returns: + True wenn Sensor, False sonst + """ + device_type = device.get('definition', '').get('uiClass', '') + controllable_name = device.get('controllableName', '') + + # Prüfung nach bekannten Typen + if device_type in cls.SENSOR_TYPES: + return True + if controllable_name in cls.SENSOR_TYPES: + return True + else: + return False + # Prüfung nach States (Sensoren haben typischerweise nur States, keine Commands) + states = device.get('states', []) + commands = device.get('definition', {}).get('commands', []) + + # Sensor hat States aber keine oder nur wenige Commands + if states and len(states) > 0 and len(commands) <= 1: + return True + + return False + + @classmethod + def extract_actor_data(cls, device: Dict) -> tuple: + """ + Extrahiert Commands und States aus einem Aktor + + Args: + device: Geräte-Dictionary von der Tahoma API + + Returns: + Tuple (commands_list, states_list) + """ + commands = [] + states = [] + + # Commands aus der Definition extrahieren + cmd_definitions = device.get('definition', {}).get('commands', []) + + for cmd in cmd_definitions: + command_name = cmd.get('commandName', '') + + command_entry = { + 'command': command_name, + 'parameters': [] + } + + # HINWEIS: Tahoma API liefert oft keine detaillierten Parameter-Infos + # Daher werden Commands erstmal ohne Parameter-Details gespeichert + #cmd_params = cmd.get('parameters', []) + cmd_params = cls.TAHOMA_COMMANDS.get(command_name,[]) + + for cmd_param in cmd_params: + param_detail = { + 'name': cmd_param.get('name', '') + } + + # Datentyp (falls vorhanden) + param_type = cmd_param.get('type') + if param_type: + param_detail['type'] = param_type + + # Min/Max Werte (meist nicht in Tahoma API vorhanden) + if 'min' in cmd_param: + param_detail['min'] = cmd_param['min'] + if 'max' in cmd_param: + param_detail['max'] = cmd_param['max'] + + # Mögliche Werte (enum) (meist nicht vorhanden) + if 'values' in cmd_param: + param_detail['values'] = cmd_param['values'] + if 'unit' in cmd_param: + param_detail['unit'] = cmd_param['unit'] + if 'description' in cmd_param: + param_detail['description'] = cmd_param['description'] + # Nur hinzufügen wenn Name vorhanden + if param_detail['name']: + command_entry['parameters'].append(param_detail) + + commands.append(command_entry) + + # States extrahieren + state_definitions = device.get('states', []) + for state in state_definitions: + state_name = state.get('name', '') + if state_name: + state_entry = { + 'name': state_name, + 'type': state.get('type', 0) + } + if 'value' in state: + state_entry['current_value'] = state['value'] + states.append(state_entry) + + return commands, states + + @classmethod + def extract_sensor_data(cls, device: Dict) -> list: + """ + Extrahiert States aus einem Sensor + + Args: + device: Geräte-Dictionary von der Tahoma API + + Returns: + Liste der States + """ + states = [] + + # States extrahieren + state_definitions = device.get('states', []) + for state in state_definitions: + state_name = state.get('name', '') + if state_name: + state_entry = { + 'name': state_name, + 'type': state.get('type', 0) + } + + # Aktueller Wert falls vorhanden + if 'value' in state: + state_entry['current_value'] = state['value'] + + states.append(state_entry) + + return states + + +def process_devices(tahoma: TahomaAPI, db: DatabaseManager, + enable_wled: bool = True, wled_timeout: int = 5, + enable_mqtt: bool = True, # NEU + clear_before_insert: bool = True): + """ + Verarbeitet alle Geräte und speichert sie in der Datenbank + + Args: + tahoma: TahomaAPI Instanz + db: DatabaseManager Instanz + enable_wled: WLED-Geräte suchen und hinzufügen + wled_timeout: Timeout für WLED-Discovery in Sekunden + clear_before_insert: Tabellen vor dem Einfügen leeren (Standard: True) + """ + # Optional: Tabellen leeren + if clear_before_insert: + db.clear_tables() + + actor_count = 0 + sensor_count = 0 + unknown_count = 0 + + # ========== TAHOMA-GERÄTE VERARBEITEN ========== + logger.info("=" * 60) + logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN") + logger.info("=" * 60) + + devices = tahoma.get_devices() + + if not devices: + logger.warning("Keine Tahoma-Geräte gefunden") + else: + # Gruppierte Geräte identifizieren (#1, #2, etc.) + device_groups = {} # {base_url: [devices]} + standalone_devices = [] + + for device in devices: + device_url = device.get('deviceURL', '') + + # Prüfen ob URL mit #1, #2, etc. endet + import re + match = re.match(r'(.+)#(\d+)$', device_url) + + if match: + base_url = match.group(1) + if base_url not in device_groups: + device_groups[base_url] = [] + device_groups[base_url].append(device) + else: + standalone_devices.append(device) + + # Gruppierte Geräte verarbeiten + for base_url, group_devices in device_groups.items(): + # Hauptgerät finden (ohne #-Endung oder mit #1) + main_device = None + sub_devices = [] + + for dev in group_devices: + url = dev.get('deviceURL', '') + if url.endswith('#1'): + main_device = dev + else: + sub_devices.append(dev) + + # Falls kein #1, nehme das erste Gerät als Hauptgerät + if not main_device and group_devices: + main_device = group_devices[0] + sub_devices = group_devices[1:] + + # Name vom Hauptgerät für alle übernehmen + main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt' + + # Alle Geräte der Gruppe verarbeiten mit gemeinsamem Namen + for device in group_devices: + device_url = device.get('deviceURL', '') + # controllableName als Typ verwenden + device_type = device.get('controllableName', device.get('uiClass', 'Unknown')) + + # Gerät klassifizieren + is_actor = DeviceClassifier.is_actor(device) + is_sensor = DeviceClassifier.is_sensor(device) + + if is_actor: + commands, states = DeviceClassifier.extract_actor_data(device) + if db.insert_actor(device_type, main_name, device_url, commands, states): + actor_count += 1 + logger.info(f"✓ Aktor [Gruppe]: {main_name} ({device_type}) - " + f"{len(commands)} Commands, {len(states)} States") + + elif is_sensor: + states = DeviceClassifier.extract_sensor_data(device) + if db.insert_sensor(device_type, main_name, device_url, states): + sensor_count += 1 + logger.info(f"✓ Sensor [Gruppe]: {main_name} ({device_type}) - " + f"{len(states)} States") + else: + unknown_count += 1 + logger.warning(f"⚠ Unbekannt [Gruppe]: {main_name} ({device_type}-{device_url})") + + # Standalone-Geräte verarbeiten + for device in standalone_devices: + device_url = device.get('deviceURL', '') + device_name = device.get('label', 'Unbekannt') + # controllableName als Typ verwenden + device_type = device.get('controllableName', device.get('uiClass', 'Unknown')) + + # Gerät klassifizieren + is_actor = DeviceClassifier.is_actor(device) + is_sensor = DeviceClassifier.is_sensor(device) + + if is_actor: + # Daten extrahieren + commands, states = DeviceClassifier.extract_actor_data(device) + + # In Datenbank speichern + if db.insert_actor(device_type, device_name, device_url, commands, states): + actor_count += 1 + logger.info(f"✓ Aktor: {device_name} ({device_type}) - " + f"{len(commands)} Commands, {len(states)} States") + + elif is_sensor: + # Daten extrahieren + states = DeviceClassifier.extract_sensor_data(device) + + # In Datenbank speichern + if db.insert_sensor(device_type, device_name, device_url, states): + sensor_count += 1 + logger.info(f"✓ Sensor: {device_name} ({device_type}) - " + f"{len(states)} States") + + else: + unknown_count += 1 + logger.warning(f"⚠ Unbekannt: {device_name} ({device_type})") + + # ========== WLED-GERÄTE SUCHEN UND VERARBEITEN ========== + if enable_wled: + logger.info("\n" + "=" * 60) + logger.info("WLED-GERÄTE WERDEN GESUCHT") + logger.info("=" * 60) + + wled_ips = WLEDDiscovery.discover_devices(timeout=wled_timeout) + + # Manuelle IPs aus Config hinzufügen + if config and config.wled_manual_ips: + logger.info(f"Füge {len(config.wled_manual_ips)} manuelle WLED-IPs hinzu...") + for manual_ip in config.wled_manual_ips: + if manual_ip not in wled_ips: + if WLEDDiscovery.is_wled_device(manual_ip): + wled_ips.append(manual_ip) + logger.info(f"✓ Manuelles WLED-Gerät: {manual_ip}") + else: + logger.warning(f"⚠ {manual_ip} ist kein WLED-Gerät") + + if not wled_ips: + logger.info("Keine WLED-Geräte gefunden") + else: + logger.info(f"\n{len(wled_ips)} WLED-Geräte gefunden, füge sie hinzu...") + + for ip in wled_ips: + try: + wled = WLEDAPI(ip) + device_data = wled.get_device_data() + + if device_data: + name = device_data['name'] + device_type = device_data['type'] + url = device_data['url'] + commands = device_data['commands'] + states = device_data['states'] + + # WLED immer als Aktor hinzufügen + if db.insert_actor(device_type, name, url, commands, states): + actor_count += 1 + logger.info(f"✓ WLED: {name} ({ip}) - " + f"{len(commands)} Commands, {len(states)} States") + else: + logger.warning(f"⚠ Konnte keine Daten von WLED {ip} abrufen") + + except Exception as e: + logger.error(f"✗ Fehler beim Verarbeiten von WLED {ip}: {e}") + # ========== MQTT-GERÄTE SUCHEN UND VERARBEITEN ========== + # ========== MQTT-GERÄTE SUCHEN UND VERARBEITEN ========== + if enable_mqtt: + logger.info("\n" + "=" * 60) + logger.info("MQTT/HOME ASSISTANT DISCOVERY") + logger.info("=" * 60) + + try: + # MQTT Discovery initialisieren + mqtt_discovery = HomeAssistantDiscovery( + broker=config.mqtt_broker, + port=config.mqtt_port, + username=config.mqtt_username, + password=config.mqtt_password, + discovery_prefix=config.mqtt_discovery_prefix + ) + + # Verbinden + if mqtt_discovery.connect(): + # Discovery durchführen + mqtt_entities = mqtt_discovery.discover_devices( + timeout=config.mqtt_discovery_timeout + ) + + # Entities nach Geräten gruppieren + mqtt_devices = MQTTDeviceConverter.group_entities_by_device(mqtt_entities) + + # Geräte verarbeiten + if not mqtt_devices: + logger.info("Keine MQTT-Geräte gefunden") + else: + logger.info(f"\n{len(mqtt_devices)} MQTT-Geräte gefunden (aus {len(mqtt_entities)} Entities)") + + for device_id, device_data in mqtt_devices.items(): + try: + # Gerät in Actor/Sensor konvertieren + actor_data, sensor_data = MQTTDeviceConverter.convert_device_to_actors_and_sensors( + device_id, device_data + ) + + # Actor speichern falls vorhanden + if actor_data: + if db.insert_actor( + actor_data['type'], + actor_data['name'], + actor_data['url'], + actor_data['commands'], + actor_data['states'] + ): + actor_count += 1 + logger.info(f"✓ MQTT Actor: {actor_data['name']} - " + f"{len(actor_data['commands'])} Commands, " + f"{len(actor_data['states'])} States") + + # Sensor speichern falls vorhanden + if sensor_data: + if db.insert_sensor( + sensor_data['type'], + sensor_data['name'], + sensor_data['url'], + sensor_data['states'] + ): + sensor_count += 1 + logger.info(f"✓ MQTT Sensor: {sensor_data['name']} - " + f"{len(sensor_data['states'])} States") + + except Exception as e: + logger.error(f"✗ Fehler beim Verarbeiten von MQTT-Gerät {device_id}: {e}") + import traceback + traceback.print_exc() + + # Verbindung trennen + mqtt_discovery.disconnect() + else: + logger.error("MQTT-Verbindung fehlgeschlagen") + + except Exception as e: + logger.error(f"✗ MQTT Discovery Fehler: {e}") + import traceback + traceback.print_exc() + + + # ========== ZUSAMMENFASSUNG ========== + logger.info("\n" + "=" * 60) + logger.info("ZUSAMMENFASSUNG") + logger.info("=" * 60) + logger.info(f"Aktoren gespeichert: {actor_count}") + logger.info(f"Sensoren gespeichert: {sensor_count}") + logger.info(f"Unbekannte Geräte: {unknown_count}") + logger.info("=" * 60) + + +def main(): + """Hauptfunktion""" + global config + + # Konfigurationsdatei laden + try: + config = Config('config.ini') + logger.info("Konfiguration erfolgreich geladen") + except FileNotFoundError as e: + print(f"FEHLER: {e}") + print("\nBitte erstellen Sie eine config.ini Datei mit Ihren Einstellungen.") + print("Siehe config.ini Vorlage für Details.") + return + except Exception as e: + print(f"FEHLER beim Laden der Konfiguration: {e}") + return + + # Logging-Level anpassen + log_level = getattr(logging, config.log_level) + logger.setLevel(log_level) + + # Optional: Log-Datei einrichten + if config.log_file: + file_handler = logging.FileHandler(config.log_file, encoding='utf-8') + file_handler.setLevel(log_level) + file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logger.addHandler(file_handler) + logger.info(f"Logging in Datei: {config.log_file}") + + # Tahoma API initialisieren + logger.info("Verbinde mit Tahoma Box...") + tahoma = TahomaAPI(config.tahoma_ip, config.tahoma_token) + + # Datenbank initialisieren + logger.info("Verbinde mit MySQL-Datenbank...") + db = DatabaseManager( + config.db_host, + config.db_name, + config.db_user, + config.db_password, + config.db_port + ) + + if not db.connect(): + logger.error("Datenbankverbindung fehlgeschlagen. Abbruch.") + return + + try: + # Geräte verarbeiten und in Datenbank speichern + process_devices( + tahoma, + db, + enable_wled=config.wled_enable, + wled_timeout=config.wled_discovery_timeout, + enable_mqtt=config.mqtt_enable, # NEU + clear_before_insert=config.clear_tables + ) + logger.info("\n✓ Import erfolgreich abgeschlossen!") + + except Exception as e: + logger.error(f"✗ Fehler während der Verarbeitung: {e}") + import traceback + traceback.print_exc() + + finally: + # Datenbankverbindung schließen + db.disconnect() + + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/restricted/history/mqtt_discovery.py b/restricted/history/mqtt_discovery.py index 9c4df23..dcf33ce 100644 --- a/restricted/history/mqtt_discovery.py +++ b/restricted/history/mqtt_discovery.py @@ -1,596 +1,596 @@ -#!/usr/bin/env python3 -""" -MQTT/Home Assistant Discovery Integration -Erweitert das Tahoma Script um MQTT-Geräte via Home Assistant Discovery -""" - -import paho.mqtt.client as mqtt -import json -import time -from typing import Dict, List, Optional -import logging - -logger = logging.getLogger(__name__) - - -class HomeAssistantDiscovery: - """Klasse für Home Assistant MQTT Discovery""" - - # Bekannte Discovery-Komponenten - COMPONENTS = [ - 'binary_sensor', 'sensor', 'switch', 'light', 'cover', - 'climate', 'fan', 'lock', 'camera', 'vacuum', 'alarm_control_panel', - 'device_tracker', 'number', 'select', 'button', 'text' - ] - - def __init__(self, broker: str, port: int = 1883, username: str = None, - password: str = None, discovery_prefix: str = 'homeassistant'): - """ - Initialisiert Home Assistant Discovery - - Args: - broker: MQTT Broker IP/Hostname - port: MQTT Port (Standard: 1883) - username: MQTT Benutzername (optional) - password: MQTT Passwort (optional) - discovery_prefix: Discovery Prefix (Standard: 'homeassistant') - """ - self.broker = broker - self.port = port - self.username = username - self.password = password - self.discovery_prefix = discovery_prefix - self.client = None - self.discovered_devices = {} - - def connect(self) -> bool: - """ - Verbindet mit dem MQTT Broker - - Returns: - True bei Erfolg, False bei Fehler - """ - try: - self.client = mqtt.Client() - - if self.username and self.password: - self.client.username_pw_set(self.username, self.password) - - self.client.on_connect = self._on_connect - self.client.on_message = self._on_message - - self.client.connect(self.broker, self.port, 60) - logger.info(f"Verbunden mit MQTT Broker {self.broker}:{self.port}") - return True - - except Exception as e: - logger.error(f"MQTT Verbindungsfehler: {e}") - return False - - def _on_connect(self, client, userdata, flags, rc): - """Callback wenn Verbindung hergestellt wurde""" - if rc == 0: - logger.info("MQTT Verbindung erfolgreich") - # Alle Discovery Topics abonnieren mit Wildcard für object_id - for component in self.COMPONENTS: - # Unterstützt beide Topic-Formate: - # homeassistant/component/node_id/config (4 Teile) - # homeassistant/component/node_id/object_id/config (5 Teile) - topic = f"{self.discovery_prefix}/{component}/+/+/config" - client.subscribe(topic) - logger.debug(f"Abonniert: {topic}") - # Zusätzlich auch das kürzere Format abonnieren - topic_short = f"{self.discovery_prefix}/{component}/+/config" - client.subscribe(topic_short) - logger.debug(f"Abonniert: {topic_short}") - else: - logger.error(f"MQTT Verbindung fehlgeschlagen, Code: {rc}") - - def _on_message(self, client, userdata, msg): - """Callback wenn Nachricht empfangen wurde""" - try: - # Topic analysieren - unterstützt beide Formate: - # homeassistant/component/node_id/config (4 Teile) - # homeassistant/component/node_id/object_id/config (5 Teile) - topic_parts = msg.topic.split('/') - - if topic_parts[-1] != 'config': - return # Kein Config-Topic - - if len(topic_parts) == 4: - # Format: homeassistant/component/node_id/config - component = topic_parts[1] - node_id = topic_parts[2] - object_id = None - elif len(topic_parts) == 5: - # Format: homeassistant/component/node_id/object_id/config - component = topic_parts[1] - node_id = topic_parts[2] - object_id = topic_parts[3] - else: - logger.debug(f"Unbekanntes Topic-Format: {msg.topic}") - return - - # Payload parsen - if msg.payload: - config = json.loads(msg.payload.decode('utf-8')) - - # Eindeutigen Key erstellen - if object_id: - device_key = f"{component}_{node_id}_{object_id}" - else: - device_key = f"{component}_{node_id}" - - # Gerät speichern - self.discovered_devices[device_key] = { - 'component': component, - 'node_id': node_id, - 'object_id': object_id, - 'config': config, - 'topic': msg.topic - } - - device_name = config.get('name', config.get('unique_id', object_id or node_id)) - logger.debug(f"Gerät gefunden: {device_name} ({component}) - {msg.topic}") - - except Exception as e: - logger.error(f"Fehler beim Verarbeiten der MQTT-Nachricht von {msg.topic}: {e}") - - def discover_devices(self, timeout: int = 10) -> Dict: - """ - Sucht nach Home Assistant Discovery Geräten - - Args: - timeout: Timeout in Sekunden - - Returns: - Dictionary mit gefundenen Geräten - """ - logger.info(f"Starte Home Assistant Discovery (Timeout: {timeout}s)...") - logger.info(f"Lausche auf {self.discovery_prefix}/+/+/+/config und {self.discovery_prefix}/+/+/config") - - self.discovered_devices = {} - - # MQTT Loop starten - self.client.loop_start() - - # Warten auf Nachrichten - mit Fortschrittsanzeige - for i in range(timeout): - time.sleep(1) - if (i + 1) % 5 == 0 or i == timeout - 1: - logger.info(f"Discovery läuft... {len(self.discovered_devices)} Geräte gefunden ({i+1}/{timeout}s)") - - # Loop stoppen - self.client.loop_stop() - - logger.info(f"✓ {len(self.discovered_devices)} MQTT-Geräte gefunden") - - # Debug: Zeige einige gefundene Topics - if self.discovered_devices: - logger.debug("Gefundene Geräte (Auswahl):") - for i, (key, device) in enumerate(list(self.discovered_devices.items())[:5]): - logger.debug(f" - {device['config'].get('name', key)} ({device['component']}) via {device['topic']}") - if len(self.discovered_devices) > 5: - logger.debug(f" ... und {len(self.discovered_devices) - 5} weitere") - - return self.discovered_devices - - def disconnect(self): - """Trennt die MQTT-Verbindung""" - if self.client: - self.client.disconnect() - logger.info("MQTT-Verbindung getrennt") - - -class MQTTDeviceConverter: - """Konvertiert MQTT Discovery Entities in Datenbank-Format, gruppiert nach Gerät""" - - # Mapping von HA Komponenten zu Actor/Sensor - ACTOR_COMPONENTS = ['switch', 'light', 'cover', 'fan', 'lock', 'climate', - 'vacuum', 'alarm_control_panel', ' ', 'number', 'select'] - SENSOR_COMPONENTS = ['binary_sensor', 'sensor', 'device_tracker'] - - @staticmethod - def group_entities_by_device(discovered_devices: Dict) -> Dict[str, List]: - """ - Gruppiert Discovery-Entities nach Gerät (node_id) - - Args: - discovered_devices: Dictionary mit allen gefundenen Entities - - Returns: - Dictionary: {device_id: [entity1, entity2, ...]} - """ - devices = {} - - for entity_key, entity in discovered_devices.items(): - # Device Identifier aus Config extrahieren - config = entity.get('config', {}) - device_info = config.get('device') or config.get('dev') or {} - # Node ID als Geräte-Identifier verwenden - node_id = entity.get('node_id', 'unknown') - - # Zusätzlich Device Identifiers prüfen falls vorhanden - if device_info and 'identifiers' in device_info: - identifiers = device_info['identifiers'] - if isinstance(identifiers, list) and identifiers: - node_id = identifiers[0] - - if node_id not in devices: - devices[node_id] = { - 'entities': [], - 'device_info': device_info, - 'node_id': node_id - } - - devices[node_id]['entities'].append(entity) - - return devices - - @staticmethod - def is_actor_entity(component: str) -> bool: - """Prüft ob Entity-Komponente ein Aktor ist""" - if(component == "climate"): - logger.info("Climate device gefunden"); - return component in MQTTDeviceConverter.ACTOR_COMPONENTS - - @staticmethod - def is_sensor_entity(component: str) -> bool: - """Prüft ob Entity-Komponente ein Sensor ist""" - return component in MQTTDeviceConverter.SENSOR_COMPONENTS - - @staticmethod - def convert_device_to_actors_and_sensors(device_id: str, device_data: Dict) -> tuple: - """ - Konvertiert ein Gerät mit allen seinen Entities in Actor/Sensor-Format - - Args: - device_id: Geräte-ID (node_id) - device_data: Device-Daten mit Entities-Liste - - Returns: - Tuple (actor_dict or None, sensor_dict or None) - """ - entities = device_data['entities'] - device_info = device_data.get('device_info') - # Gerätename vom ersten Entity oder aus device_info - device_name = device_info.get('name', device_id) - if not device_name or device_name == device_id: - # Fallback: Name vom ersten Entity - if entities: - device_name = entities[0]['config'].get('name', device_id) - - # Device URL - device_url = f"mqtt://{device_id}" - - # Entities nach Actor/Sensor trennen - actor_entities = [e for e in entities if MQTTDeviceConverter.is_actor_entity(e['component'])] - sensor_entities = [e for e in entities if MQTTDeviceConverter.is_sensor_entity(e['component'])] - - actor_result = None - sensor_result = None - - # Actor erstellen falls Actor-Entities vorhanden - if actor_entities: - commands = [] - states = [] - - for entity in actor_entities: - component = entity['component'] - object_id = entity.get('object_id', 'unknown') - config = entity['config'] - - # Command aus der Entity erstellen - command_entry = MQTTDeviceConverter._entity_to_command(component, object_id, config) - if command_entry: - commands.append(command_entry) - - # States aus der Entity extrahieren - entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) - states.extend(entity_states) - - if commands: # Nur Actor erstellen wenn Commands vorhanden - actor_result = { - 'name': device_name, - 'type': f"mqtt_device", # Allgemeiner Typ für Multi-Entity-Geräte - 'url': device_url, - 'commands': commands, - 'states': states - } - - # Sensor erstellen falls Sensor-Entities vorhanden - if sensor_entities: - states = [] - - for entity in sensor_entities: - component = entity['component'] - object_id = entity.get('object_id', 'unknown') - config = entity['config'] - - # States aus der Entity extrahieren - entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) - states.extend(entity_states) - - if states: # Nur Sensor erstellen wenn States vorhanden - sensor_result = { - 'name': device_name, - 'type': f"mqtt_device", - 'url': device_url, - 'states': states - } - - return actor_result, sensor_result - - @staticmethod - def _entity_to_command(component: str, object_id: str, config: Dict) -> Optional[Dict]: - """ - Konvertiert eine MQTT Entity in ein Command - - Args: - component: Entity-Typ (number, button, switch, etc.) - object_id: Object ID (z.B. set_max_ampere_limit) - config: Entity-Konfiguration - - Returns: - Command-Dictionary oder None - """ - # Command Topic - verschiedene mögliche Feldnamen - command_topic = config.get('command_topic') or config.get('cmd_t') or config.get('temperature_command_topic') - - if not command_topic: - return None - - # Command-Name aus object_id ableiten - command_name = object_id.replace('_', ' ').title().replace(' ', '') - # Oder aus dem Namen - entity_name = config.get('name', object_id) - - command_entry = { - 'command': command_name, - 'parameters': [] - } - - # Parameter basierend auf Component-Typ - if component == 'number': - # Number hat einen Wert-Parameter - param = { - 'name': 'value', - 'type': 'number', - 'url': command_topic - } - - # Min/Max aus Config - if 'min' in config: - param['min'] = config['min'] - if 'max' in config: - param['max'] = config['max'] - - # Unit hinzufügen - verschiedene mögliche Feldnamen - unit = ( - config.get('unit_of_measurement') or - config.get('unit_of_meas') or - config.get('unit') or - config.get('u') - ) - if unit: - param['unit'] = unit - - command_entry['parameters'].append(param) - - elif component == 'select': - # Select hat Optionen - param = { - 'name': 'option', - 'type': 'string', - 'url': command_topic - } - - options = config.get('options') or config.get('ops') - if options: - param['values'] = options - - command_entry['parameters'].append(param) - - elif component in ['switch', 'light']: - # Switch/Light haben on/off - param = { - 'name': 'state', - 'type': 'string', - 'url': command_topic, - 'values': [ - config.get('payload_on', config.get('pl_on', 'ON')), - config.get('payload_off', config.get('pl_off', 'OFF')) - ] - } - command_entry['parameters'].append(param) - - # Brightness für Light - brightness_cmd_topic = ( - config.get('brightness_command_topic') or - config.get('bri_cmd_t') - ) - if component == 'light' and brightness_cmd_topic: - command_entry['parameters'].append({ - 'name': 'brightness', - 'type': 'integer', - 'min': 0, - 'max': 255, - 'url': brightness_cmd_topic - }) - - elif component == 'cover': - # Cover hat position - set_pos_topic = ( - config.get('set_position_topic') or - config.get('pos_cmd_t') - ) - if set_pos_topic: - param = { - 'name': 'position', - 'type': 'integer', - 'min': 0, - 'max': 100, - 'url': set_pos_topic - } - command_entry['parameters'].append(param) - else: - # Nur open/close/stop - param = { - 'name': 'action', - 'type': 'string', - 'url': command_topic, - 'values': ['OPEN', 'CLOSE', 'STOP'] - } - command_entry['parameters'].append(param) - - elif component == 'button': - # Button hat normalerweise keinen Parameter, nur das Topic - param = { - 'name': 'press', - 'type': 'trigger', - 'url': command_topic - } - command_entry['parameters'].append(param) - - elif component == 'climate': - # Climate hat Temperatur-Setpoint - temp_cmd_topic = ( - config.get('temperature_command_topic') or - config.get('temp_cmd_t') - ) - if temp_cmd_topic: - param = { - 'name': 'temperature', - 'type': 'number', - 'url': temp_cmd_topic - } - - if 'min_temp' in config: - param['min'] = config['min_temp'] - if 'max_temp' in config: - param['max'] = config['max_temp'] - - command_entry['parameters'].append(param) - mode_cmd_topic = ( - config.get('mode_command_topic') or - config.get('mode_cmd_t') - ) - if mode_cmd_topic: - param = { - 'name': 'mode', - 'type': 'string', - 'url': temp_cmd_topic, - 'values': config.get('modes', []) - } - else: - # Generischer Command mit dem Topic - param = { - 'name': 'value', - 'type': 'string', - 'url': command_topic - } - command_entry['parameters'].append(param) - - return command_entry - - @staticmethod - def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]: - """ - Extrahiert States aus einer MQTT Entity - - Args: - component: Entity-Typ - object_id: Object ID - config: Entity-Konfiguration - - Returns: - Liste von State-Dictionaries - """ - states = [] - - # State Topic - verschiedene mögliche Feldnamen prüfen - state_topic = ( - config.get('state_topic') or - config.get('stat_t') or # Abkürzung - config.get('~') and config.get('stat_t') # Mit Base Topic - ) - - # Bei number/select: oft kein separates state_topic, dann command_topic verwenden - if not state_topic and component in ['number', 'select', 'button']: - # Bei diesen Komponenten kann der State über command_topic abgefragt werden - # oder es gibt ein explizites state_topic - state_topic = config.get('command_topic') or config.get('cmd_t') - - if state_topic: - state_entry = { - 'name': object_id, - 'type': 'string', - 'url': state_topic - } - - # Unit hinzufügen - verschiedene mögliche Feldnamen - unit = ( - config.get('unit_of_measurement') or - config.get('unit_of_meas') or - config.get('unit') or - config.get('u') # Weitere Abkürzung - ) - if unit: - state_entry['unit'] = unit - - # Device Class als zusätzliche Info - if 'device_class' in config: - state_entry['device_class'] = config['device_class'] - elif 'dev_cla' in config: - state_entry['device_class'] = config['dev_cla'] - - # Typ anpassen basierend auf Component - if component == 'number': - state_entry['type'] = 'number' - elif component == 'binary_sensor': - state_entry['type'] = 'boolean' - elif component == 'sensor': - # Bei Sensor den Typ aus value_template ableiten oder number annehmen - state_entry['type'] = 'number' # Default für Sensoren - - states.append(state_entry) - - # Zusätzliche State Topics (z.B. brightness bei Light) - if component == 'light': - brightness_topic = ( - config.get('brightness_state_topic') or - config.get('bri_stat_t') - ) - if brightness_topic: - states.append({ - 'name': f"{object_id}_brightness", - 'type': 'integer', - 'url': brightness_topic - }) - - if component == 'cover': - position_topic = ( - config.get('position_topic') or - config.get('pos_t') - ) - if position_topic: - states.append({ - 'name': f"{object_id}_position", - 'type': 'integer', - 'url': position_topic - }) - - if component == 'climate': - current_temp_topic = ( - config.get('current_temperature_topic') or - config.get('curr_temp_t') - ) - if current_temp_topic: - states.append({ - 'name': f"{object_id}_current_temp", - 'type': 'number', - 'unit': '°C', - 'url': current_temp_topic - }) - +#!/usr/bin/env python3 +""" +MQTT/Home Assistant Discovery Integration +Erweitert das Tahoma Script um MQTT-Geräte via Home Assistant Discovery +""" + +import paho.mqtt.client as mqtt +import json +import time +from typing import Dict, List, Optional +import logging + +logger = logging.getLogger(__name__) + + +class HomeAssistantDiscovery: + """Klasse für Home Assistant MQTT Discovery""" + + # Bekannte Discovery-Komponenten + COMPONENTS = [ + 'binary_sensor', 'sensor', 'switch', 'light', 'cover', + 'climate', 'fan', 'lock', 'camera', 'vacuum', 'alarm_control_panel', + 'device_tracker', 'number', 'select', 'button', 'text' + ] + + def __init__(self, broker: str, port: int = 1883, username: str = None, + password: str = None, discovery_prefix: str = 'homeassistant'): + """ + Initialisiert Home Assistant Discovery + + Args: + broker: MQTT Broker IP/Hostname + port: MQTT Port (Standard: 1883) + username: MQTT Benutzername (optional) + password: MQTT Passwort (optional) + discovery_prefix: Discovery Prefix (Standard: 'homeassistant') + """ + self.broker = broker + self.port = port + self.username = username + self.password = password + self.discovery_prefix = discovery_prefix + self.client = None + self.discovered_devices = {} + + def connect(self) -> bool: + """ + Verbindet mit dem MQTT Broker + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + self.client = mqtt.Client() + + if self.username and self.password: + self.client.username_pw_set(self.username, self.password) + + self.client.on_connect = self._on_connect + self.client.on_message = self._on_message + + self.client.connect(self.broker, self.port, 60) + logger.info(f"Verbunden mit MQTT Broker {self.broker}:{self.port}") + return True + + except Exception as e: + logger.error(f"MQTT Verbindungsfehler: {e}") + return False + + def _on_connect(self, client, userdata, flags, rc): + """Callback wenn Verbindung hergestellt wurde""" + if rc == 0: + logger.info("MQTT Verbindung erfolgreich") + # Alle Discovery Topics abonnieren mit Wildcard für object_id + for component in self.COMPONENTS: + # Unterstützt beide Topic-Formate: + # homeassistant/component/node_id/config (4 Teile) + # homeassistant/component/node_id/object_id/config (5 Teile) + topic = f"{self.discovery_prefix}/{component}/+/+/config" + client.subscribe(topic) + logger.debug(f"Abonniert: {topic}") + # Zusätzlich auch das kürzere Format abonnieren + topic_short = f"{self.discovery_prefix}/{component}/+/config" + client.subscribe(topic_short) + logger.debug(f"Abonniert: {topic_short}") + else: + logger.error(f"MQTT Verbindung fehlgeschlagen, Code: {rc}") + + def _on_message(self, client, userdata, msg): + """Callback wenn Nachricht empfangen wurde""" + try: + # Topic analysieren - unterstützt beide Formate: + # homeassistant/component/node_id/config (4 Teile) + # homeassistant/component/node_id/object_id/config (5 Teile) + topic_parts = msg.topic.split('/') + + if topic_parts[-1] != 'config': + return # Kein Config-Topic + + if len(topic_parts) == 4: + # Format: homeassistant/component/node_id/config + component = topic_parts[1] + node_id = topic_parts[2] + object_id = None + elif len(topic_parts) == 5: + # Format: homeassistant/component/node_id/object_id/config + component = topic_parts[1] + node_id = topic_parts[2] + object_id = topic_parts[3] + else: + logger.debug(f"Unbekanntes Topic-Format: {msg.topic}") + return + + # Payload parsen + if msg.payload: + config = json.loads(msg.payload.decode('utf-8')) + + # Eindeutigen Key erstellen + if object_id: + device_key = f"{component}_{node_id}_{object_id}" + else: + device_key = f"{component}_{node_id}" + + # Gerät speichern + self.discovered_devices[device_key] = { + 'component': component, + 'node_id': node_id, + 'object_id': object_id, + 'config': config, + 'topic': msg.topic + } + + device_name = config.get('name', config.get('unique_id', object_id or node_id)) + logger.debug(f"Gerät gefunden: {device_name} ({component}) - {msg.topic}") + + except Exception as e: + logger.error(f"Fehler beim Verarbeiten der MQTT-Nachricht von {msg.topic}: {e}") + + def discover_devices(self, timeout: int = 10) -> Dict: + """ + Sucht nach Home Assistant Discovery Geräten + + Args: + timeout: Timeout in Sekunden + + Returns: + Dictionary mit gefundenen Geräten + """ + logger.info(f"Starte Home Assistant Discovery (Timeout: {timeout}s)...") + logger.info(f"Lausche auf {self.discovery_prefix}/+/+/+/config und {self.discovery_prefix}/+/+/config") + + self.discovered_devices = {} + + # MQTT Loop starten + self.client.loop_start() + + # Warten auf Nachrichten - mit Fortschrittsanzeige + for i in range(timeout): + time.sleep(1) + if (i + 1) % 5 == 0 or i == timeout - 1: + logger.info(f"Discovery läuft... {len(self.discovered_devices)} Geräte gefunden ({i+1}/{timeout}s)") + + # Loop stoppen + self.client.loop_stop() + + logger.info(f"✓ {len(self.discovered_devices)} MQTT-Geräte gefunden") + + # Debug: Zeige einige gefundene Topics + if self.discovered_devices: + logger.debug("Gefundene Geräte (Auswahl):") + for i, (key, device) in enumerate(list(self.discovered_devices.items())[:5]): + logger.debug(f" - {device['config'].get('name', key)} ({device['component']}) via {device['topic']}") + if len(self.discovered_devices) > 5: + logger.debug(f" ... und {len(self.discovered_devices) - 5} weitere") + + return self.discovered_devices + + def disconnect(self): + """Trennt die MQTT-Verbindung""" + if self.client: + self.client.disconnect() + logger.info("MQTT-Verbindung getrennt") + + +class MQTTDeviceConverter: + """Konvertiert MQTT Discovery Entities in Datenbank-Format, gruppiert nach Gerät""" + + # Mapping von HA Komponenten zu Actor/Sensor + ACTOR_COMPONENTS = ['switch', 'light', 'cover', 'fan', 'lock', 'climate', + 'vacuum', 'alarm_control_panel', ' ', 'number', 'select'] + SENSOR_COMPONENTS = ['binary_sensor', 'sensor', 'device_tracker'] + + @staticmethod + def group_entities_by_device(discovered_devices: Dict) -> Dict[str, List]: + """ + Gruppiert Discovery-Entities nach Gerät (node_id) + + Args: + discovered_devices: Dictionary mit allen gefundenen Entities + + Returns: + Dictionary: {device_id: [entity1, entity2, ...]} + """ + devices = {} + + for entity_key, entity in discovered_devices.items(): + # Device Identifier aus Config extrahieren + config = entity.get('config', {}) + device_info = config.get('device') or config.get('dev') or {} + # Node ID als Geräte-Identifier verwenden + node_id = entity.get('node_id', 'unknown') + + # Zusätzlich Device Identifiers prüfen falls vorhanden + if device_info and 'identifiers' in device_info: + identifiers = device_info['identifiers'] + if isinstance(identifiers, list) and identifiers: + node_id = identifiers[0] + + if node_id not in devices: + devices[node_id] = { + 'entities': [], + 'device_info': device_info, + 'node_id': node_id + } + + devices[node_id]['entities'].append(entity) + + return devices + + @staticmethod + def is_actor_entity(component: str) -> bool: + """Prüft ob Entity-Komponente ein Aktor ist""" + if(component == "climate"): + logger.info("Climate device gefunden"); + return component in MQTTDeviceConverter.ACTOR_COMPONENTS + + @staticmethod + def is_sensor_entity(component: str) -> bool: + """Prüft ob Entity-Komponente ein Sensor ist""" + return component in MQTTDeviceConverter.SENSOR_COMPONENTS + + @staticmethod + def convert_device_to_actors_and_sensors(device_id: str, device_data: Dict) -> tuple: + """ + Konvertiert ein Gerät mit allen seinen Entities in Actor/Sensor-Format + + Args: + device_id: Geräte-ID (node_id) + device_data: Device-Daten mit Entities-Liste + + Returns: + Tuple (actor_dict or None, sensor_dict or None) + """ + entities = device_data['entities'] + device_info = device_data.get('device_info') + # Gerätename vom ersten Entity oder aus device_info + device_name = device_info.get('name', device_id) + if not device_name or device_name == device_id: + # Fallback: Name vom ersten Entity + if entities: + device_name = entities[0]['config'].get('name', device_id) + + # Device URL + device_url = f"mqtt://{device_id}" + + # Entities nach Actor/Sensor trennen + actor_entities = [e for e in entities if MQTTDeviceConverter.is_actor_entity(e['component'])] + sensor_entities = [e for e in entities if MQTTDeviceConverter.is_sensor_entity(e['component'])] + + actor_result = None + sensor_result = None + + # Actor erstellen falls Actor-Entities vorhanden + if actor_entities: + commands = [] + states = [] + + for entity in actor_entities: + component = entity['component'] + object_id = entity.get('object_id', 'unknown') + config = entity['config'] + + # Command aus der Entity erstellen + command_entry = MQTTDeviceConverter._entity_to_command(component, object_id, config) + if command_entry: + commands.append(command_entry) + + # States aus der Entity extrahieren + entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) + states.extend(entity_states) + + if commands: # Nur Actor erstellen wenn Commands vorhanden + actor_result = { + 'name': device_name, + 'type': f"mqtt_device", # Allgemeiner Typ für Multi-Entity-Geräte + 'url': device_url, + 'commands': commands, + 'states': states + } + + # Sensor erstellen falls Sensor-Entities vorhanden + if sensor_entities: + states = [] + + for entity in sensor_entities: + component = entity['component'] + object_id = entity.get('object_id', 'unknown') + config = entity['config'] + + # States aus der Entity extrahieren + entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config) + states.extend(entity_states) + + if states: # Nur Sensor erstellen wenn States vorhanden + sensor_result = { + 'name': device_name, + 'type': f"mqtt_device", + 'url': device_url, + 'states': states + } + + return actor_result, sensor_result + + @staticmethod + def _entity_to_command(component: str, object_id: str, config: Dict) -> Optional[Dict]: + """ + Konvertiert eine MQTT Entity in ein Command + + Args: + component: Entity-Typ (number, button, switch, etc.) + object_id: Object ID (z.B. set_max_ampere_limit) + config: Entity-Konfiguration + + Returns: + Command-Dictionary oder None + """ + # Command Topic - verschiedene mögliche Feldnamen + command_topic = config.get('command_topic') or config.get('cmd_t') or config.get('temperature_command_topic') + + if not command_topic: + return None + + # Command-Name aus object_id ableiten + command_name = object_id.replace('_', ' ').title().replace(' ', '') + # Oder aus dem Namen + entity_name = config.get('name', object_id) + + command_entry = { + 'command': command_name, + 'parameters': [] + } + + # Parameter basierend auf Component-Typ + if component == 'number': + # Number hat einen Wert-Parameter + param = { + 'name': 'value', + 'type': 'number', + 'url': command_topic + } + + # Min/Max aus Config + if 'min' in config: + param['min'] = config['min'] + if 'max' in config: + param['max'] = config['max'] + + # Unit hinzufügen - verschiedene mögliche Feldnamen + unit = ( + config.get('unit_of_measurement') or + config.get('unit_of_meas') or + config.get('unit') or + config.get('u') + ) + if unit: + param['unit'] = unit + + command_entry['parameters'].append(param) + + elif component == 'select': + # Select hat Optionen + param = { + 'name': 'option', + 'type': 'string', + 'url': command_topic + } + + options = config.get('options') or config.get('ops') + if options: + param['values'] = options + + command_entry['parameters'].append(param) + + elif component in ['switch', 'light']: + # Switch/Light haben on/off + param = { + 'name': 'state', + 'type': 'string', + 'url': command_topic, + 'values': [ + config.get('payload_on', config.get('pl_on', 'ON')), + config.get('payload_off', config.get('pl_off', 'OFF')) + ] + } + command_entry['parameters'].append(param) + + # Brightness für Light + brightness_cmd_topic = ( + config.get('brightness_command_topic') or + config.get('bri_cmd_t') + ) + if component == 'light' and brightness_cmd_topic: + command_entry['parameters'].append({ + 'name': 'brightness', + 'type': 'integer', + 'min': 0, + 'max': 255, + 'url': brightness_cmd_topic + }) + + elif component == 'cover': + # Cover hat position + set_pos_topic = ( + config.get('set_position_topic') or + config.get('pos_cmd_t') + ) + if set_pos_topic: + param = { + 'name': 'position', + 'type': 'integer', + 'min': 0, + 'max': 100, + 'url': set_pos_topic + } + command_entry['parameters'].append(param) + else: + # Nur open/close/stop + param = { + 'name': 'action', + 'type': 'string', + 'url': command_topic, + 'values': ['OPEN', 'CLOSE', 'STOP'] + } + command_entry['parameters'].append(param) + + elif component == 'button': + # Button hat normalerweise keinen Parameter, nur das Topic + param = { + 'name': 'press', + 'type': 'trigger', + 'url': command_topic + } + command_entry['parameters'].append(param) + + elif component == 'climate': + # Climate hat Temperatur-Setpoint + temp_cmd_topic = ( + config.get('temperature_command_topic') or + config.get('temp_cmd_t') + ) + if temp_cmd_topic: + param = { + 'name': 'temperature', + 'type': 'number', + 'url': temp_cmd_topic + } + + if 'min_temp' in config: + param['min'] = config['min_temp'] + if 'max_temp' in config: + param['max'] = config['max_temp'] + + command_entry['parameters'].append(param) + mode_cmd_topic = ( + config.get('mode_command_topic') or + config.get('mode_cmd_t') + ) + if mode_cmd_topic: + param = { + 'name': 'mode', + 'type': 'string', + 'url': temp_cmd_topic, + 'values': config.get('modes', []) + } + else: + # Generischer Command mit dem Topic + param = { + 'name': 'value', + 'type': 'string', + 'url': command_topic + } + command_entry['parameters'].append(param) + + return command_entry + + @staticmethod + def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]: + """ + Extrahiert States aus einer MQTT Entity + + Args: + component: Entity-Typ + object_id: Object ID + config: Entity-Konfiguration + + Returns: + Liste von State-Dictionaries + """ + states = [] + + # State Topic - verschiedene mögliche Feldnamen prüfen + state_topic = ( + config.get('state_topic') or + config.get('stat_t') or # Abkürzung + config.get('~') and config.get('stat_t') # Mit Base Topic + ) + + # Bei number/select: oft kein separates state_topic, dann command_topic verwenden + if not state_topic and component in ['number', 'select', 'button']: + # Bei diesen Komponenten kann der State über command_topic abgefragt werden + # oder es gibt ein explizites state_topic + state_topic = config.get('command_topic') or config.get('cmd_t') + + if state_topic: + state_entry = { + 'name': object_id, + 'type': 'string', + 'url': state_topic + } + + # Unit hinzufügen - verschiedene mögliche Feldnamen + unit = ( + config.get('unit_of_measurement') or + config.get('unit_of_meas') or + config.get('unit') or + config.get('u') # Weitere Abkürzung + ) + if unit: + state_entry['unit'] = unit + + # Device Class als zusätzliche Info + if 'device_class' in config: + state_entry['device_class'] = config['device_class'] + elif 'dev_cla' in config: + state_entry['device_class'] = config['dev_cla'] + + # Typ anpassen basierend auf Component + if component == 'number': + state_entry['type'] = 'number' + elif component == 'binary_sensor': + state_entry['type'] = 'boolean' + elif component == 'sensor': + # Bei Sensor den Typ aus value_template ableiten oder number annehmen + state_entry['type'] = 'number' # Default für Sensoren + + states.append(state_entry) + + # Zusätzliche State Topics (z.B. brightness bei Light) + if component == 'light': + brightness_topic = ( + config.get('brightness_state_topic') or + config.get('bri_stat_t') + ) + if brightness_topic: + states.append({ + 'name': f"{object_id}_brightness", + 'type': 'integer', + 'url': brightness_topic + }) + + if component == 'cover': + position_topic = ( + config.get('position_topic') or + config.get('pos_t') + ) + if position_topic: + states.append({ + 'name': f"{object_id}_position", + 'type': 'integer', + 'url': position_topic + }) + + if component == 'climate': + current_temp_topic = ( + config.get('current_temperature_topic') or + config.get('curr_temp_t') + ) + if current_temp_topic: + states.append({ + 'name': f"{object_id}_current_temp", + 'type': 'number', + 'unit': '°C', + 'url': current_temp_topic + }) + return states \ No newline at end of file diff --git a/restricted/history/shelly_discovery.py b/restricted/history/shelly_discovery.py index 3fa2c2e..af3596e 100644 --- a/restricted/history/shelly_discovery.py +++ b/restricted/history/shelly_discovery.py @@ -1,756 +1,756 @@ -#!/usr/bin/env python3 -""" -Shelly Device Discovery Script -Findet alle Shelly-Geräte im lokalen Netzwerk und speichert Sensoren und Aktoren -in der Datenbank gemäß dem EnergyFlow Schema. -""" - -import json -import requests -import socket -import mysql.connector -from mysql.connector import Error -from typing import List, Dict, Optional -import argparse -import logging -from datetime import datetime - -# Logging konfigurieren -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - - -class ShellyDiscovery: - """Klasse zum Entdecken von Shelly-Geräten im Netzwerk""" - - SHELLY_MDNS_SERVICE = "_http._tcp.local." - COMMON_PORTS = [80] - - def __init__(self, network_range: str = "192.168.1"): - self.network_range = network_range - self.devices = [] - - def scan_network(self, start_ip: int = 1, end_ip: int = 254, timeout: float = 0.5) -> List[str]: - """ - Scannt das Netzwerk nach aktiven Hosts - - Args: - start_ip: Start IP (letztes Oktett) - end_ip: End IP (letztes Oktett) - timeout: Timeout für Socket-Verbindung - - Returns: - Liste von erreichbaren IP-Adressen - """ - active_hosts = [] - logger.info(f"Scanne Netzwerk {self.network_range}.{start_ip}-{end_ip}...") - - for i in range(start_ip, end_ip + 1): - ip = f"{self.network_range}.{i}" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - - try: - result = sock.connect_ex((ip, 80)) - if result == 0: - active_hosts.append(ip) - logger.debug(f"Host gefunden: {ip}") - except: - pass - finally: - sock.close() - - logger.info(f"{len(active_hosts)} aktive Hosts gefunden") - return active_hosts - - def is_shelly_device(self, ip: str) -> Optional[Dict]: - """ - Prüft ob ein Host ein Shelly-Gerät ist - - Args: - ip: IP-Adresse des Hosts - - Returns: - Device Info Dict wenn Shelly, sonst None - """ - try: - # Versuche Gen2 API (neuere Shelly-Geräte) - response = requests.get( - f"http://{ip}/rpc/Shelly.GetDeviceInfo", - timeout=2 - ) - if response.status_code == 200: - data = response.json() - logger.info(f"Shelly Gen2 Gerät gefunden: {ip} - {data.get('name', 'Unknown')}") - return { - 'ip': ip, - 'generation': 2, - 'info': data - } - except: - pass - - try: - # Versuche Gen1 API (ältere Shelly-Geräte) - response = requests.get( - f"http://{ip}/shelly", - timeout=2 - ) - if response.status_code == 200: - data = response.json() - if 'type' in data and data['type'].startswith('SHELLY'): - logger.info(f"Shelly Gen1 Gerät gefunden: {ip} - {data.get('type', 'Unknown')}") - return { - 'ip': ip, - 'generation': 1, - 'info': data - } - except: - pass - - return None - - def get_device_status(self, device: Dict) -> Optional[Dict]: - """ - Holt den Status eines Shelly-Geräts - - Args: - device: Device Info Dictionary - - Returns: - Status Dictionary oder None - """ - ip = device['ip'] - - try: - if device['generation'] == 2: - # Gen2 Status - response = requests.get( - f"http://{ip}/rpc/Shelly.GetStatus", - timeout=2 - ) - if response.status_code == 200: - return response.json() - else: - # Gen1 Status - response = requests.get( - f"http://{ip}/status", - timeout=2 - ) - if response.status_code == 200: - return response.json() - except Exception as e: - logger.error(f"Fehler beim Abrufen des Status von {ip}: {e}") - - return None - - def discover_devices(self, start_ip: int = 1, end_ip: int = 254) -> List[Dict]: - """ - Entdeckt alle Shelly-Geräte im Netzwerk - - Returns: - Liste von Shelly-Geräten mit Status - """ - active_hosts = self.scan_network(start_ip, end_ip) - - for ip in active_hosts: - device = self.is_shelly_device(ip) - if device: - status = self.get_device_status(device) - device['status'] = status - self.devices.append(device) - - logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt") - return self.devices - - -class ShellyDatabaseWriter: - """Klasse zum Schreiben der Shelly-Daten in die Datenbank""" - - def __init__(self, host: str, user: str, password: str, database: str): - self.host = host - self.user = user - self.password = password - self.database = database - self.connection = None - - def connect(self): - """Stellt Verbindung zur Datenbank her""" - try: - self.connection = mysql.connector.connect( - host=self.host, - user=self.user, - password=self.password, - database=self.database, - charset='utf8mb4', - collation='utf8mb4_bin' - ) - logger.info("Datenbankverbindung hergestellt") - except Error as e: - logger.error(f"Fehler bei Datenbankverbindung: {e}") - raise - - def disconnect(self): - """Schließt Datenbankverbindung""" - if self.connection and self.connection.is_connected(): - self.connection.close() - logger.info("Datenbankverbindung geschlossen") - - def parse_gen2_device(self, device: Dict) -> tuple: - """ - Parst Gen2 Shelly-Gerät und extrahiert Aktoren/Sensoren - - Returns: - (actors, sensors) Tuple mit Listen - """ - actors = [] - sensors = [] - - info = device.get('info', {}) - status = device.get('status', {}) - ip = device['ip'] - - device_name = info.get('name', f"Shelly_{info.get('id', ip)}") - device_model = info.get('model', 'Unknown') - - # Switches als Aktoren - if 'switch:0' in status or 'switch' in status: - switch_count = 0 - for key in status.keys(): - if key.startswith('switch:'): - switch_count += 1 - - for i in range(switch_count): - switch_data = status.get(f'switch:{i}', {}) - actors.append({ - 'type': f'ShellySwitch_{device_model}', - 'name': f"{device_name}_Switch_{i}", - 'url': f"http://{ip}/rpc/Switch.Set?id={i}", - 'parameters': json.dumps({ - 'device_id': info.get('id'), - 'switch_id': i, - 'model': device_model, - 'generation': 2 - }), - 'commands': [ - {'command_name': 'turn_on', 'params': []}, - {'command_name': 'turn_off', 'params': []}, - {'command_name': 'toggle', 'params': []} - ], - 'states': [ - { - 'state_name': 'output', - 'current_value': str(switch_data.get('output', False)), - 'unit': None - } - ] - }) - - # Cover/Roller als Aktoren - if 'cover:0' in status: - cover_count = 0 - for key in status.keys(): - if key.startswith('cover:'): - cover_count += 1 - - for i in range(cover_count): - cover_data = status.get(f'cover:{i}', {}) - actors.append({ - 'type': f'ShellyCover_{device_model}', - 'name': f"{device_name}_Cover_{i}", - 'url': f"http://{ip}/rpc/Cover.GoToPosition?id={i}", - 'parameters': json.dumps({ - 'device_id': info.get('id'), - 'cover_id': i, - 'model': device_model, - 'generation': 2 - }), - 'commands': [ - {'command_name': 'open', 'params': []}, - {'command_name': 'close', 'params': []}, - {'command_name': 'stop', 'params': []}, - {'command_name': 'set_position', 'params': [ - { - 'parameter_name': 'position', - 'parameter_type': 'integer', - 'min_value': 0, - 'max_value': 100 - } - ]} - ], - 'states': [ - { - 'state_name': 'current_pos', - 'current_value': str(cover_data.get('current_pos', 0)), - 'unit': '%' - }, - { - 'state_name': 'state', - 'current_value': cover_data.get('state', 'unknown'), - 'unit': None - } - ] - }) - - # Temperatur-Sensoren - for key in status.keys(): - if key.startswith('temperature:'): - temp_id = key.split(':')[1] - temp_data = status[key] - sensors.append({ - 'type': 'ShellyTemperatureSensor', - 'name': f"{device_name}_Temperature_{temp_id}", - 'url': f"http://{ip}/rpc/Temperature.GetStatus?id={temp_id}", - 'parameters': json.dumps({ - 'device_id': info.get('id'), - 'sensor_id': temp_id, - 'model': device_model - }), - 'states': [ - { - 'state_name': 'temperature', - 'current_value': str(temp_data.get('tC', 0)), - 'unit': '°C' - } - ] - }) - - # Humidity-Sensoren - for key in status.keys(): - if key.startswith('humidity:'): - hum_id = key.split(':')[1] - hum_data = status[key] - sensors.append({ - 'type': 'ShellyHumiditySensor', - 'name': f"{device_name}_Humidity_{hum_id}", - 'url': f"http://{ip}/rpc/Humidity.GetStatus?id={hum_id}", - 'parameters': json.dumps({ - 'device_id': info.get('id'), - 'sensor_id': hum_id, - 'model': device_model - }), - 'states': [ - { - 'state_name': 'humidity', - 'current_value': str(hum_data.get('rh', 0)), - 'unit': '%' - } - ] - }) - - # Energie-Sensoren (Power Meter) - for i in range(10): # Max 10 switches/covers prüfen - switch_key = f'switch:{i}' - if switch_key in status: - switch_data = status[switch_key] - if 'apower' in switch_data: # Aktuelle Leistung - sensors.append({ - 'type': 'ShellyPowerMeter', - 'name': f"{device_name}_Power_{i}", - 'url': f"http://{ip}/rpc/Switch.GetStatus?id={i}", - 'parameters': json.dumps({ - 'device_id': info.get('id'), - 'switch_id': i, - 'model': device_model - }), - 'states': [ - { - 'state_name': 'active_power', - 'current_value': str(switch_data.get('apower', 0)), - 'unit': 'W' - }, - { - 'state_name': 'voltage', - 'current_value': str(switch_data.get('voltage', 0)), - 'unit': 'V' - }, - { - 'state_name': 'current', - 'current_value': str(switch_data.get('current', 0)), - 'unit': 'A' - } - ] - }) - - return actors, sensors - - def parse_gen1_device(self, device: Dict) -> tuple: - """ - Parst Gen1 Shelly-Gerät und extrahiert Aktoren/Sensoren - - Returns: - (actors, sensors) Tuple mit Listen - """ - actors = [] - sensors = [] - - info = device.get('info', {}) - status = device.get('status', {}) - ip = device['ip'] - - device_type = info.get('type', 'Unknown') - device_name = info.get('name', f"Shelly_{device_type}_{ip}") - - # Relays als Aktoren - if 'relays' in status: - for i, relay in enumerate(status['relays']): - actors.append({ - 'type': f'ShellyRelay_{device_type}', - 'name': f"{device_name}_Relay_{i}", - 'url': f"http://{ip}/relay/{i}", - 'parameters': json.dumps({ - 'device_type': device_type, - 'relay_id': i, - 'generation': 1 - }), - 'commands': [ - {'command_name': 'turn_on', 'params': []}, - {'command_name': 'turn_off', 'params': []}, - {'command_name': 'toggle', 'params': []} - ], - 'states': [ - { - 'state_name': 'ison', - 'current_value': str(relay.get('ison', False)), - 'unit': None - } - ] - }) - - # Rollers als Aktoren - if 'rollers' in status: - for i, roller in enumerate(status['rollers']): - actors.append({ - 'type': f'ShellyRoller_{device_type}', - 'name': f"{device_name}_Roller_{i}", - 'url': f"http://{ip}/roller/{i}", - 'parameters': json.dumps({ - 'device_type': device_type, - 'roller_id': i, - 'generation': 1 - }), - 'commands': [ - {'command_name': 'open', 'params': []}, - {'command_name': 'close', 'params': []}, - {'command_name': 'stop', 'params': []}, - {'command_name': 'go_to_position', 'params': [ - { - 'parameter_name': 'position', - 'parameter_type': 'integer', - 'min_value': 0, - 'max_value': 100 - } - ]} - ], - 'states': [ - { - 'state_name': 'current_pos', - 'current_value': str(roller.get('current_pos', 0)), - 'unit': '%' - }, - { - 'state_name': 'state', - 'current_value': roller.get('state', 'stop'), - 'unit': None - } - ] - }) - - # Temperatur-Sensoren - if 'tmp' in status: - temp_data = status['tmp'] - if 'tC' in temp_data: - sensors.append({ - 'type': 'ShellyTemperatureSensor', - 'name': f"{device_name}_Temperature", - 'url': f"http://{ip}/status", - 'parameters': json.dumps({ - 'device_type': device_type, - 'generation': 1 - }), - 'states': [ - { - 'state_name': 'temperature', - 'current_value': str(temp_data.get('tC', 0)), - 'unit': '°C' - } - ] - }) - - # Energie-Sensoren (Meters) - if 'meters' in status: - for i, meter in enumerate(status['meters']): - sensors.append({ - 'type': 'ShellyPowerMeter', - 'name': f"{device_name}_Power_{i}", - 'url': f"http://{ip}/status", - 'parameters': json.dumps({ - 'device_type': device_type, - 'meter_id': i, - 'generation': 1 - }), - 'states': [ - { - 'state_name': 'power', - 'current_value': str(meter.get('power', 0)), - 'unit': 'W' - }, - { - 'state_name': 'total', - 'current_value': str(meter.get('total', 0)), - 'unit': 'Wh' - } - ] - }) - - return actors, sensors - - def insert_actor(self, actor: Dict) -> Optional[int]: - """ - Fügt einen Aktor in die Datenbank ein - - Returns: - Actor ID oder None bei Fehler - """ - try: - cursor = self.connection.cursor() - - # Prüfe ob Aktor bereits existiert - cursor.execute( - "SELECT id FROM actors WHERE url = %s", - (actor['url'],) - ) - result = cursor.fetchone() - - if result: - actor_id = result[0] - # Update bestehender Aktor - cursor.execute( - """UPDATE actors - SET type = %s, name = %s, parameters = %s - WHERE id = %s""", - (actor['type'], actor['name'], actor['parameters'], actor_id) - ) - logger.info(f"Aktor aktualisiert: {actor['name']}") - else: - # Neuer Aktor - cursor.execute( - """INSERT INTO actors (type, name, parameters, url) - VALUES (%s, %s, %s, %s)""", - (actor['type'], actor['name'], actor['parameters'], actor['url']) - ) - actor_id = cursor.lastrowid - logger.info(f"Neuer Aktor eingefügt: {actor['name']}") - - # Commands einfügen - for command in actor.get('commands', []): - cursor.execute( - """INSERT INTO actor_commands (actor_id, command_name) - VALUES (%s, %s) - ON DUPLICATE KEY UPDATE command_name = command_name""", - (actor_id, command['command_name']) - ) - command_id = cursor.lastrowid - - # Command Parameters einfügen - for param in command.get('params', []): - cursor.execute( - """INSERT INTO command_parameters - (command_id, parameter_name, parameter_type, min_value, max_value) - VALUES (%s, %s, %s, %s, %s)""", - (command_id, param['parameter_name'], param['parameter_type'], - param.get('min_value'), param.get('max_value')) - ) - - # States einfügen - for state in actor.get('states', []): - cursor.execute( - """INSERT INTO actor_states - (actor_id, state_name, current_value, unit) - VALUES (%s, %s, %s, %s) - ON DUPLICATE KEY UPDATE - current_value = VALUES(current_value), - last_updated = CURRENT_TIMESTAMP""", - (actor_id, state['state_name'], state['current_value'], state['unit']) - ) - - self.connection.commit() - cursor.close() - return actor_id - - except Error as e: - logger.error(f"Fehler beim Einfügen des Aktors: {e}") - self.connection.rollback() - return None - - def insert_sensor(self, sensor: Dict) -> Optional[int]: - """ - Fügt einen Sensor in die Datenbank ein - - Returns: - Sensor ID oder None bei Fehler - """ - try: - cursor = self.connection.cursor() - - # Prüfe ob Sensor bereits existiert - cursor.execute( - "SELECT id FROM sensors WHERE url = %s", - (sensor['url'],) - ) - result = cursor.fetchone() - - if result: - sensor_id = result[0] - # Update bestehender Sensor - cursor.execute( - """UPDATE sensors - SET type = %s, name = %s, parameters = %s - WHERE id = %s""", - (sensor['type'], sensor['name'], sensor['parameters'], sensor_id) - ) - logger.info(f"Sensor aktualisiert: {sensor['name']}") - else: - # Neuer Sensor - cursor.execute( - """INSERT INTO sensors (type, name, parameters, url) - VALUES (%s, %s, %s, %s)""", - (sensor['type'], sensor['name'], sensor['parameters'], sensor['url']) - ) - sensor_id = cursor.lastrowid - logger.info(f"Neuer Sensor eingefügt: {sensor['name']}") - - # States einfügen - for state in sensor.get('states', []): - cursor.execute( - """INSERT INTO sensor_states - (sensor_id, state_name, current_value, unit) - VALUES (%s, %s, %s, %s) - ON DUPLICATE KEY UPDATE - current_value = VALUES(current_value), - last_updated = CURRENT_TIMESTAMP""", - (sensor_id, state['state_name'], state['current_value'], state['unit']) - ) - - self.connection.commit() - cursor.close() - return sensor_id - - except Error as e: - logger.error(f"Fehler beim Einfügen des Sensors: {e}") - self.connection.rollback() - return None - - def process_devices(self, devices: List[Dict]): - """ - Verarbeitet alle entdeckten Geräte und schreibt sie in die DB - """ - total_actors = 0 - total_sensors = 0 - - for device in devices: - logger.info(f"Verarbeite Gerät: {device['ip']}") - - if device['generation'] == 2: - actors, sensors = self.parse_gen2_device(device) - else: - actors, sensors = self.parse_gen1_device(device) - - # Aktoren einfügen - for actor in actors: - if self.insert_actor(actor): - total_actors += 1 - - # Sensoren einfügen - for sensor in sensors: - if self.insert_sensor(sensor): - total_sensors += 1 - - logger.info(f"Verarbeitung abgeschlossen: {total_actors} Aktoren, {total_sensors} Sensoren") - - -def main(): - """Hauptfunktion""" - parser = argparse.ArgumentParser( - description='Findet Shelly-Geräte im Netzwerk und schreibt sie in die Datenbank' - ) - parser.add_argument( - '--network', - default='192.168.1', - help='Netzwerk-Präfix (Standard: 192.168.1)' - ) - parser.add_argument( - '--start-ip', - type=int, - default=1, - help='Start IP (letztes Oktett, Standard: 1)' - ) - parser.add_argument( - '--end-ip', - type=int, - default=254, - help='End IP (letztes Oktett, Standard: 254)' - ) - parser.add_argument( - '--db-host', - default='localhost', - help='Datenbank Host (Standard: localhost)' - ) - parser.add_argument( - '--db-user', - required=True, - help='Datenbank Benutzer' - ) - parser.add_argument( - '--db-password', - required=True, - help='Datenbank Passwort' - ) - parser.add_argument( - '--db-name', - default='EnergyFlow', - help='Datenbank Name (Standard: EnergyFlow)' - ) - parser.add_argument( - '--debug', - action='store_true', - help='Debug-Modus aktivieren' - ) - - args = parser.parse_args() - - if args.debug: - logger.setLevel(logging.DEBUG) - - # Discovery - logger.info("Starte Shelly Device Discovery...") - discovery = ShellyDiscovery(network_range=args.network) - devices = discovery.discover_devices(start_ip=args.start_ip, end_ip=args.end_ip) - - if not devices: - logger.warning("Keine Shelly-Geräte gefunden!") - return - - # Datenbank-Schreibvorgang - logger.info("Schreibe Geräte in Datenbank...") - db_writer = ShellyDatabaseWriter( - host=args.db_host, - user=args.db_user, - password=args.db_password, - database=args.db_name - ) - - try: - db_writer.connect() - db_writer.process_devices(devices) - finally: - db_writer.disconnect() - - logger.info("Fertig!") - - -if __name__ == '__main__': +#!/usr/bin/env python3 +""" +Shelly Device Discovery Script +Findet alle Shelly-Geräte im lokalen Netzwerk und speichert Sensoren und Aktoren +in der Datenbank gemäß dem EnergyFlow Schema. +""" + +import json +import requests +import socket +import mysql.connector +from mysql.connector import Error +from typing import List, Dict, Optional +import argparse +import logging +from datetime import datetime + +# Logging konfigurieren +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class ShellyDiscovery: + """Klasse zum Entdecken von Shelly-Geräten im Netzwerk""" + + SHELLY_MDNS_SERVICE = "_http._tcp.local." + COMMON_PORTS = [80] + + def __init__(self, network_range: str = "192.168.1"): + self.network_range = network_range + self.devices = [] + + def scan_network(self, start_ip: int = 1, end_ip: int = 254, timeout: float = 0.5) -> List[str]: + """ + Scannt das Netzwerk nach aktiven Hosts + + Args: + start_ip: Start IP (letztes Oktett) + end_ip: End IP (letztes Oktett) + timeout: Timeout für Socket-Verbindung + + Returns: + Liste von erreichbaren IP-Adressen + """ + active_hosts = [] + logger.info(f"Scanne Netzwerk {self.network_range}.{start_ip}-{end_ip}...") + + for i in range(start_ip, end_ip + 1): + ip = f"{self.network_range}.{i}" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + + try: + result = sock.connect_ex((ip, 80)) + if result == 0: + active_hosts.append(ip) + logger.debug(f"Host gefunden: {ip}") + except: + pass + finally: + sock.close() + + logger.info(f"{len(active_hosts)} aktive Hosts gefunden") + return active_hosts + + def is_shelly_device(self, ip: str) -> Optional[Dict]: + """ + Prüft ob ein Host ein Shelly-Gerät ist + + Args: + ip: IP-Adresse des Hosts + + Returns: + Device Info Dict wenn Shelly, sonst None + """ + try: + # Versuche Gen2 API (neuere Shelly-Geräte) + response = requests.get( + f"http://{ip}/rpc/Shelly.GetDeviceInfo", + timeout=2 + ) + if response.status_code == 200: + data = response.json() + logger.info(f"Shelly Gen2 Gerät gefunden: {ip} - {data.get('name', 'Unknown')}") + return { + 'ip': ip, + 'generation': 2, + 'info': data + } + except: + pass + + try: + # Versuche Gen1 API (ältere Shelly-Geräte) + response = requests.get( + f"http://{ip}/shelly", + timeout=2 + ) + if response.status_code == 200: + data = response.json() + if 'type' in data and data['type'].startswith('SHELLY'): + logger.info(f"Shelly Gen1 Gerät gefunden: {ip} - {data.get('type', 'Unknown')}") + return { + 'ip': ip, + 'generation': 1, + 'info': data + } + except: + pass + + return None + + def get_device_status(self, device: Dict) -> Optional[Dict]: + """ + Holt den Status eines Shelly-Geräts + + Args: + device: Device Info Dictionary + + Returns: + Status Dictionary oder None + """ + ip = device['ip'] + + try: + if device['generation'] == 2: + # Gen2 Status + response = requests.get( + f"http://{ip}/rpc/Shelly.GetStatus", + timeout=2 + ) + if response.status_code == 200: + return response.json() + else: + # Gen1 Status + response = requests.get( + f"http://{ip}/status", + timeout=2 + ) + if response.status_code == 200: + return response.json() + except Exception as e: + logger.error(f"Fehler beim Abrufen des Status von {ip}: {e}") + + return None + + def discover_devices(self, start_ip: int = 1, end_ip: int = 254) -> List[Dict]: + """ + Entdeckt alle Shelly-Geräte im Netzwerk + + Returns: + Liste von Shelly-Geräten mit Status + """ + active_hosts = self.scan_network(start_ip, end_ip) + + for ip in active_hosts: + device = self.is_shelly_device(ip) + if device: + status = self.get_device_status(device) + device['status'] = status + self.devices.append(device) + + logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt") + return self.devices + + +class ShellyDatabaseWriter: + """Klasse zum Schreiben der Shelly-Daten in die Datenbank""" + + def __init__(self, host: str, user: str, password: str, database: str): + self.host = host + self.user = user + self.password = password + self.database = database + self.connection = None + + def connect(self): + """Stellt Verbindung zur Datenbank her""" + try: + self.connection = mysql.connector.connect( + host=self.host, + user=self.user, + password=self.password, + database=self.database, + charset='utf8mb4', + collation='utf8mb4_bin' + ) + logger.info("Datenbankverbindung hergestellt") + except Error as e: + logger.error(f"Fehler bei Datenbankverbindung: {e}") + raise + + def disconnect(self): + """Schließt Datenbankverbindung""" + if self.connection and self.connection.is_connected(): + self.connection.close() + logger.info("Datenbankverbindung geschlossen") + + def parse_gen2_device(self, device: Dict) -> tuple: + """ + Parst Gen2 Shelly-Gerät und extrahiert Aktoren/Sensoren + + Returns: + (actors, sensors) Tuple mit Listen + """ + actors = [] + sensors = [] + + info = device.get('info', {}) + status = device.get('status', {}) + ip = device['ip'] + + device_name = info.get('name', f"Shelly_{info.get('id', ip)}") + device_model = info.get('model', 'Unknown') + + # Switches als Aktoren + if 'switch:0' in status or 'switch' in status: + switch_count = 0 + for key in status.keys(): + if key.startswith('switch:'): + switch_count += 1 + + for i in range(switch_count): + switch_data = status.get(f'switch:{i}', {}) + actors.append({ + 'type': f'ShellySwitch_{device_model}', + 'name': f"{device_name}_Switch_{i}", + 'url': f"http://{ip}/rpc/Switch.Set?id={i}", + 'parameters': json.dumps({ + 'device_id': info.get('id'), + 'switch_id': i, + 'model': device_model, + 'generation': 2 + }), + 'commands': [ + {'command_name': 'turn_on', 'params': []}, + {'command_name': 'turn_off', 'params': []}, + {'command_name': 'toggle', 'params': []} + ], + 'states': [ + { + 'state_name': 'output', + 'current_value': str(switch_data.get('output', False)), + 'unit': None + } + ] + }) + + # Cover/Roller als Aktoren + if 'cover:0' in status: + cover_count = 0 + for key in status.keys(): + if key.startswith('cover:'): + cover_count += 1 + + for i in range(cover_count): + cover_data = status.get(f'cover:{i}', {}) + actors.append({ + 'type': f'ShellyCover_{device_model}', + 'name': f"{device_name}_Cover_{i}", + 'url': f"http://{ip}/rpc/Cover.GoToPosition?id={i}", + 'parameters': json.dumps({ + 'device_id': info.get('id'), + 'cover_id': i, + 'model': device_model, + 'generation': 2 + }), + 'commands': [ + {'command_name': 'open', 'params': []}, + {'command_name': 'close', 'params': []}, + {'command_name': 'stop', 'params': []}, + {'command_name': 'set_position', 'params': [ + { + 'parameter_name': 'position', + 'parameter_type': 'integer', + 'min_value': 0, + 'max_value': 100 + } + ]} + ], + 'states': [ + { + 'state_name': 'current_pos', + 'current_value': str(cover_data.get('current_pos', 0)), + 'unit': '%' + }, + { + 'state_name': 'state', + 'current_value': cover_data.get('state', 'unknown'), + 'unit': None + } + ] + }) + + # Temperatur-Sensoren + for key in status.keys(): + if key.startswith('temperature:'): + temp_id = key.split(':')[1] + temp_data = status[key] + sensors.append({ + 'type': 'ShellyTemperatureSensor', + 'name': f"{device_name}_Temperature_{temp_id}", + 'url': f"http://{ip}/rpc/Temperature.GetStatus?id={temp_id}", + 'parameters': json.dumps({ + 'device_id': info.get('id'), + 'sensor_id': temp_id, + 'model': device_model + }), + 'states': [ + { + 'state_name': 'temperature', + 'current_value': str(temp_data.get('tC', 0)), + 'unit': '°C' + } + ] + }) + + # Humidity-Sensoren + for key in status.keys(): + if key.startswith('humidity:'): + hum_id = key.split(':')[1] + hum_data = status[key] + sensors.append({ + 'type': 'ShellyHumiditySensor', + 'name': f"{device_name}_Humidity_{hum_id}", + 'url': f"http://{ip}/rpc/Humidity.GetStatus?id={hum_id}", + 'parameters': json.dumps({ + 'device_id': info.get('id'), + 'sensor_id': hum_id, + 'model': device_model + }), + 'states': [ + { + 'state_name': 'humidity', + 'current_value': str(hum_data.get('rh', 0)), + 'unit': '%' + } + ] + }) + + # Energie-Sensoren (Power Meter) + for i in range(10): # Max 10 switches/covers prüfen + switch_key = f'switch:{i}' + if switch_key in status: + switch_data = status[switch_key] + if 'apower' in switch_data: # Aktuelle Leistung + sensors.append({ + 'type': 'ShellyPowerMeter', + 'name': f"{device_name}_Power_{i}", + 'url': f"http://{ip}/rpc/Switch.GetStatus?id={i}", + 'parameters': json.dumps({ + 'device_id': info.get('id'), + 'switch_id': i, + 'model': device_model + }), + 'states': [ + { + 'state_name': 'active_power', + 'current_value': str(switch_data.get('apower', 0)), + 'unit': 'W' + }, + { + 'state_name': 'voltage', + 'current_value': str(switch_data.get('voltage', 0)), + 'unit': 'V' + }, + { + 'state_name': 'current', + 'current_value': str(switch_data.get('current', 0)), + 'unit': 'A' + } + ] + }) + + return actors, sensors + + def parse_gen1_device(self, device: Dict) -> tuple: + """ + Parst Gen1 Shelly-Gerät und extrahiert Aktoren/Sensoren + + Returns: + (actors, sensors) Tuple mit Listen + """ + actors = [] + sensors = [] + + info = device.get('info', {}) + status = device.get('status', {}) + ip = device['ip'] + + device_type = info.get('type', 'Unknown') + device_name = info.get('name', f"Shelly_{device_type}_{ip}") + + # Relays als Aktoren + if 'relays' in status: + for i, relay in enumerate(status['relays']): + actors.append({ + 'type': f'ShellyRelay_{device_type}', + 'name': f"{device_name}_Relay_{i}", + 'url': f"http://{ip}/relay/{i}", + 'parameters': json.dumps({ + 'device_type': device_type, + 'relay_id': i, + 'generation': 1 + }), + 'commands': [ + {'command_name': 'turn_on', 'params': []}, + {'command_name': 'turn_off', 'params': []}, + {'command_name': 'toggle', 'params': []} + ], + 'states': [ + { + 'state_name': 'ison', + 'current_value': str(relay.get('ison', False)), + 'unit': None + } + ] + }) + + # Rollers als Aktoren + if 'rollers' in status: + for i, roller in enumerate(status['rollers']): + actors.append({ + 'type': f'ShellyRoller_{device_type}', + 'name': f"{device_name}_Roller_{i}", + 'url': f"http://{ip}/roller/{i}", + 'parameters': json.dumps({ + 'device_type': device_type, + 'roller_id': i, + 'generation': 1 + }), + 'commands': [ + {'command_name': 'open', 'params': []}, + {'command_name': 'close', 'params': []}, + {'command_name': 'stop', 'params': []}, + {'command_name': 'go_to_position', 'params': [ + { + 'parameter_name': 'position', + 'parameter_type': 'integer', + 'min_value': 0, + 'max_value': 100 + } + ]} + ], + 'states': [ + { + 'state_name': 'current_pos', + 'current_value': str(roller.get('current_pos', 0)), + 'unit': '%' + }, + { + 'state_name': 'state', + 'current_value': roller.get('state', 'stop'), + 'unit': None + } + ] + }) + + # Temperatur-Sensoren + if 'tmp' in status: + temp_data = status['tmp'] + if 'tC' in temp_data: + sensors.append({ + 'type': 'ShellyTemperatureSensor', + 'name': f"{device_name}_Temperature", + 'url': f"http://{ip}/status", + 'parameters': json.dumps({ + 'device_type': device_type, + 'generation': 1 + }), + 'states': [ + { + 'state_name': 'temperature', + 'current_value': str(temp_data.get('tC', 0)), + 'unit': '°C' + } + ] + }) + + # Energie-Sensoren (Meters) + if 'meters' in status: + for i, meter in enumerate(status['meters']): + sensors.append({ + 'type': 'ShellyPowerMeter', + 'name': f"{device_name}_Power_{i}", + 'url': f"http://{ip}/status", + 'parameters': json.dumps({ + 'device_type': device_type, + 'meter_id': i, + 'generation': 1 + }), + 'states': [ + { + 'state_name': 'power', + 'current_value': str(meter.get('power', 0)), + 'unit': 'W' + }, + { + 'state_name': 'total', + 'current_value': str(meter.get('total', 0)), + 'unit': 'Wh' + } + ] + }) + + return actors, sensors + + def insert_actor(self, actor: Dict) -> Optional[int]: + """ + Fügt einen Aktor in die Datenbank ein + + Returns: + Actor ID oder None bei Fehler + """ + try: + cursor = self.connection.cursor() + + # Prüfe ob Aktor bereits existiert + cursor.execute( + "SELECT id FROM actors WHERE url = %s", + (actor['url'],) + ) + result = cursor.fetchone() + + if result: + actor_id = result[0] + # Update bestehender Aktor + cursor.execute( + """UPDATE actors + SET type = %s, name = %s, parameters = %s + WHERE id = %s""", + (actor['type'], actor['name'], actor['parameters'], actor_id) + ) + logger.info(f"Aktor aktualisiert: {actor['name']}") + else: + # Neuer Aktor + cursor.execute( + """INSERT INTO actors (type, name, parameters, url) + VALUES (%s, %s, %s, %s)""", + (actor['type'], actor['name'], actor['parameters'], actor['url']) + ) + actor_id = cursor.lastrowid + logger.info(f"Neuer Aktor eingefügt: {actor['name']}") + + # Commands einfügen + for command in actor.get('commands', []): + cursor.execute( + """INSERT INTO actor_commands (actor_id, command_name) + VALUES (%s, %s) + ON DUPLICATE KEY UPDATE command_name = command_name""", + (actor_id, command['command_name']) + ) + command_id = cursor.lastrowid + + # Command Parameters einfügen + for param in command.get('params', []): + cursor.execute( + """INSERT INTO command_parameters + (command_id, parameter_name, parameter_type, min_value, max_value) + VALUES (%s, %s, %s, %s, %s)""", + (command_id, param['parameter_name'], param['parameter_type'], + param.get('min_value'), param.get('max_value')) + ) + + # States einfügen + for state in actor.get('states', []): + cursor.execute( + """INSERT INTO actor_states + (actor_id, state_name, current_value, unit) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + current_value = VALUES(current_value), + last_updated = CURRENT_TIMESTAMP""", + (actor_id, state['state_name'], state['current_value'], state['unit']) + ) + + self.connection.commit() + cursor.close() + return actor_id + + except Error as e: + logger.error(f"Fehler beim Einfügen des Aktors: {e}") + self.connection.rollback() + return None + + def insert_sensor(self, sensor: Dict) -> Optional[int]: + """ + Fügt einen Sensor in die Datenbank ein + + Returns: + Sensor ID oder None bei Fehler + """ + try: + cursor = self.connection.cursor() + + # Prüfe ob Sensor bereits existiert + cursor.execute( + "SELECT id FROM sensors WHERE url = %s", + (sensor['url'],) + ) + result = cursor.fetchone() + + if result: + sensor_id = result[0] + # Update bestehender Sensor + cursor.execute( + """UPDATE sensors + SET type = %s, name = %s, parameters = %s + WHERE id = %s""", + (sensor['type'], sensor['name'], sensor['parameters'], sensor_id) + ) + logger.info(f"Sensor aktualisiert: {sensor['name']}") + else: + # Neuer Sensor + cursor.execute( + """INSERT INTO sensors (type, name, parameters, url) + VALUES (%s, %s, %s, %s)""", + (sensor['type'], sensor['name'], sensor['parameters'], sensor['url']) + ) + sensor_id = cursor.lastrowid + logger.info(f"Neuer Sensor eingefügt: {sensor['name']}") + + # States einfügen + for state in sensor.get('states', []): + cursor.execute( + """INSERT INTO sensor_states + (sensor_id, state_name, current_value, unit) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + current_value = VALUES(current_value), + last_updated = CURRENT_TIMESTAMP""", + (sensor_id, state['state_name'], state['current_value'], state['unit']) + ) + + self.connection.commit() + cursor.close() + return sensor_id + + except Error as e: + logger.error(f"Fehler beim Einfügen des Sensors: {e}") + self.connection.rollback() + return None + + def process_devices(self, devices: List[Dict]): + """ + Verarbeitet alle entdeckten Geräte und schreibt sie in die DB + """ + total_actors = 0 + total_sensors = 0 + + for device in devices: + logger.info(f"Verarbeite Gerät: {device['ip']}") + + if device['generation'] == 2: + actors, sensors = self.parse_gen2_device(device) + else: + actors, sensors = self.parse_gen1_device(device) + + # Aktoren einfügen + for actor in actors: + if self.insert_actor(actor): + total_actors += 1 + + # Sensoren einfügen + for sensor in sensors: + if self.insert_sensor(sensor): + total_sensors += 1 + + logger.info(f"Verarbeitung abgeschlossen: {total_actors} Aktoren, {total_sensors} Sensoren") + + +def main(): + """Hauptfunktion""" + parser = argparse.ArgumentParser( + description='Findet Shelly-Geräte im Netzwerk und schreibt sie in die Datenbank' + ) + parser.add_argument( + '--network', + default='192.168.1', + help='Netzwerk-Präfix (Standard: 192.168.1)' + ) + parser.add_argument( + '--start-ip', + type=int, + default=1, + help='Start IP (letztes Oktett, Standard: 1)' + ) + parser.add_argument( + '--end-ip', + type=int, + default=254, + help='End IP (letztes Oktett, Standard: 254)' + ) + parser.add_argument( + '--db-host', + default='localhost', + help='Datenbank Host (Standard: localhost)' + ) + parser.add_argument( + '--db-user', + required=True, + help='Datenbank Benutzer' + ) + parser.add_argument( + '--db-password', + required=True, + help='Datenbank Passwort' + ) + parser.add_argument( + '--db-name', + default='EnergyFlow', + help='Datenbank Name (Standard: EnergyFlow)' + ) + parser.add_argument( + '--debug', + action='store_true', + help='Debug-Modus aktivieren' + ) + + args = parser.parse_args() + + if args.debug: + logger.setLevel(logging.DEBUG) + + # Discovery + logger.info("Starte Shelly Device Discovery...") + discovery = ShellyDiscovery(network_range=args.network) + devices = discovery.discover_devices(start_ip=args.start_ip, end_ip=args.end_ip) + + if not devices: + logger.warning("Keine Shelly-Geräte gefunden!") + return + + # Datenbank-Schreibvorgang + logger.info("Schreibe Geräte in Datenbank...") + db_writer = ShellyDatabaseWriter( + host=args.db_host, + user=args.db_user, + password=args.db_password, + database=args.db_name + ) + + try: + db_writer.connect() + db_writer.process_devices(devices) + finally: + db_writer.disconnect() + + logger.info("Fertig!") + + +if __name__ == '__main__': main() \ No newline at end of file diff --git a/restricted/home.php b/restricted/home.php index 069c1cc..6cf0b0c 100644 --- a/restricted/home.php +++ b/restricted/home.php @@ -1,235 +1,235 @@ - - -
- -
- -
- -
-
-

Home

-
-
- -
- -
- - -
- -
- -
- -
-
-
-

Home

-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - > - - dur="0.7s" - repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1" fill="freeze" /> - - - - - 1 - ? min(100, (295 - 61) / (count($etagen) - 1)) - : 0; - // Von Hand mittig gerueckt, je nach Breite der Buchstaben. - $textX = ["OG" => 62, "EG" => 64, "UG" => 63, "AG" => 62]; -?> - $etage): ?> - - - - - - - - > - - [12], 2 => [9, 16], 3 => [5, 11.5, 16.5]]; - $y = $zeilen[count($room["werte"])] ?? []; -?> - ,)" onclick="openHeaterSettings('_');" cursor="pointer"> - - - _heater" href="#heater" display="none" /> - _buffer" href="#buffer" display="none" /> - - - - 8 && $luecke !== false) - ? [substr($room["mqtt"], 0, $luecke), substr($room["mqtt"], $luecke + 1)] - : [$room["mqtt"]]; - $namenszeilen = count($teile) === 2 ? [9, 15.5] : [11.5]; -?> - $teil): ?> - - - - $wert): ?> - - " x="12" y="" - text-anchor="middle">– - - - - - - - - -
-
-
- -
-
-
-
-

Automatismen

-
- - - -
- -
-
- -
- -
" - id="actions-" role="tabpanel" aria-labelledby="actions--tab"> - -
Wird geladen...
- -
- -
-
-
-
- -
- - -
- -
- -
- - - + + +
+ +
+ +
+ +
+
+

Home

+
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
+

Home

+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + > + + dur="0.7s" + repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1" fill="freeze" /> + + + + + 1 + ? min(100, (295 - 61) / (count($etagen) - 1)) + : 0; + // Von Hand mittig gerueckt, je nach Breite der Buchstaben. + $textX = ["OG" => 62, "EG" => 64, "UG" => 63, "AG" => 62]; +?> + $etage): ?> + + + + + + + + > + + [12], 2 => [9, 16], 3 => [5, 11.5, 16.5]]; + $y = $zeilen[count($room["werte"])] ?? []; +?> + ,)" onclick="openHeaterSettings('_');" cursor="pointer"> + + + _heater" href="#heater" display="none" /> + _buffer" href="#buffer" display="none" /> + + + + 8 && $luecke !== false) + ? [substr($room["mqtt"], 0, $luecke), substr($room["mqtt"], $luecke + 1)] + : [$room["mqtt"]]; + $namenszeilen = count($teile) === 2 ? [9, 15.5] : [11.5]; +?> + $teil): ?> + + + + $wert): ?> + + " x="12" y="" + text-anchor="middle">– + + + + + + + + +
+
+
+ +
+
+
+
+

Automatismen

+
+ + + +
+ +
+
+ +
+ +
" + id="actions-" role="tabpanel" aria-labelledby="actions--tab"> + +
Wird geladen...
+ +
+ +
+
+
+
+ +
+ + +
+ +
+ +
+ + + diff --git a/restricted/tahoma_EG.php b/restricted/tahoma_EG.php index 23a88bf..382df79 100644 --- a/restricted/tahoma_EG.php +++ b/restricted/tahoma_EG.php @@ -1,7 +1,7 @@ - \ No newline at end of file