diff --git a/classes/web_server.py b/classes/web_server.py
index 86b7fc6..6baa83a 100644
--- a/classes/web_server.py
+++ b/classes/web_server.py
@@ -25,7 +25,29 @@ def do_GET(self): # pylint: disable=invalid-name
try:
request_path, get_vars = self.parse_get_vars()
- if request_path == "/get/is_alive":
+ if request_path == '/dial':
+ parsed = urllib.parse.urlparse(self.path)
+ template = self.stargate.cfg.get("ui_dialing_template")
+
+ if template == 'dialing retro v1':
+ new_path = "/retro/dial.html"
+ else:
+ new_path = "/index.htm"
+ self.send_response(302) # Temporary redirect
+ self.send_header("Location", new_path + ("?" + parsed.query if parsed.query else ""))
+ self.end_headers()
+ return
+ elif request_path == '/address_book':
+ template = self.stargate.cfg.get("ui_address_book_template")
+ self.send_response(302) # Temporary redirect
+ if template == 'address retro v1':
+ self.send_header("Location", "/retro/address_book.html")
+ else:
+ self.send_header("Location", "/address_book.htm")
+ self.end_headers()
+ return
+
+ elif request_path == "/get/is_alive":
data = { 'is_alive': True }
elif request_path == "/get/address_book":
diff --git a/config/defaults-milkyway/config.json.dist b/config/defaults-milkyway/config.json.dist
index 97b1b13..2fe185b 100644
--- a/config/defaults-milkyway/config.json.dist
+++ b/config/defaults-milkyway/config.json.dist
@@ -369,5 +369,23 @@
"min_value": 0,
"max_value": false,
"units": "Minutes"
+ },
+ "ui_dialing_template": {
+ "value": "default",
+ "desc": "What page design to use for Home/Dialing",
+ "type": "str-enum",
+ "enum_values": [
+ "default",
+ "dialing retro v1"
+ ]
+ },
+ "ui_address_book_template": {
+ "value": "default",
+ "desc": "What page design to use for Address Book",
+ "type": "str-enum",
+ "enum_values": [
+ "default",
+ "address retro v1"
+ ]
}
}
diff --git a/web/address_book.htm b/web/address_book.htm
index 9080893..3d67517 100644
--- a/web/address_book.htm
+++ b/web/address_book.htm
@@ -49,7 +49,7 @@
-
- Home
+ Home
-
Address Book
diff --git a/web/config.htm b/web/config.htm
index a7508dd..34cfcc2 100644
--- a/web/config.htm
+++ b/web/config.htm
@@ -52,10 +52,10 @@
-
- Home
+ Home
-
- Address Book
+ Address Book
-
Symbols
diff --git a/web/debug.htm b/web/debug.htm
index 73ef2fa..03619bd 100644
--- a/web/debug.htm
+++ b/web/debug.htm
@@ -51,10 +51,10 @@
-
- Home
+ Home
-
- Address Book
+ Address Book
-
Symbols
diff --git a/web/help.htm b/web/help.htm
index a042dea..6eea0e5 100644
--- a/web/help.htm
+++ b/web/help.htm
@@ -48,10 +48,10 @@
-
- Home
+ Home
-
- Address Book
+ Address Book
-
Symbols
diff --git a/web/index.htm b/web/index.htm
index 3f5fefb..3201281 100644
--- a/web/index.htm
+++ b/web/index.htm
@@ -53,7 +53,7 @@
Home
-
- Address Book
+ Address Book
-
Symbols
diff --git a/web/info.htm b/web/info.htm
index f119c40..c454a32 100644
--- a/web/info.htm
+++ b/web/info.htm
@@ -472,10 +472,10 @@
-
- Home
+ Home
-
- Address Book
+ Address Book
-
Symbols
diff --git a/web/js/address_book.js b/web/js/address_book.js
index 3948479..77d9934 100644
--- a/web/js/address_book.js
+++ b/web/js/address_book.js
@@ -46,7 +46,7 @@ function load_address_book(){
address = address_raw.join("");
- $("#presets").append('
' + value.name + '
' + address + '
' + is_gate_online + '
' );
});
}
diff --git a/web/js/config.js b/web/js/config.js
index be3bb4f..64e416d 100644
--- a/web/js/config.js
+++ b/web/js/config.js
@@ -61,7 +61,8 @@ function getParamGroupByPrettyName(paramPrettyName){
"Software": "Software Update",
"Stepper": "Stepper",
"Subspace": "Subspace Network",
- "Wormhole": "Wormhole Max Time"
+ "Wormhole": "Wormhole Max Time",
+ "Ui": "UI Templates"
}
compound_groups = {
diff --git a/web/retro/address_book.html b/web/retro/address_book.html
new file mode 100644
index 0000000..473600f
--- /dev/null
+++ b/web/retro/address_book.html
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+ Address Book | Stargate Command
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ PROFILE
+
+
+
+
+
+

+
Virgo
+
+
+

+
Serpens Caput
+
+
+

+
Virgo
+
+
+

+
Virgo
+
+
+

+
Virgo
+
+
+

+
Virgo
+
+
+

+
Earth
+
+
+
+
+
+
+
+ Gate Type:Fan
+
+
+ Status:Online
+
+
+ Coords:18.06.10.33
+
+
+ Case:325423543
+
+
+ Ref:325423543
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/retro/css/address_book.css b/web/retro/css/address_book.css
new file mode 100644
index 0000000..4ee9a27
--- /dev/null
+++ b/web/retro/css/address_book.css
@@ -0,0 +1,503 @@
+@charset "UTF-8";
+:root {
+ /*! UPDATE COLORS HERE !!! -------------------------------------------------------------------------------------- */
+ --color: #37bfde;
+ --color-dark: #4a7297;
+ --color-danger: #c70036;
+ --color-good: #07ff0b;
+ --color-alt: white;
+ --background-color: #020f25;
+ --glyph-color: #fffea5;
+ /*! Calculate glyph-color-svg here: https://codepen.io/sosuke/pen/Pjoqqp */
+ --glyph-color-svg: invert(100%) sepia(21%) saturate(5367%) hue-rotate(312deg)
+ brightness(111%) contrast(103%);
+ --glyph-color-danger-svg: invert(16%) sepia(84%) saturate(3541%)
+ hue-rotate(331deg) brightness(81%) contrast(117%);
+ --color-wormhole-danger-1: yellow;
+ --color-wormhole-danger-2: orange;
+ --color-wormhole-danger-3: red;
+ --color-wormhole-1: royalblue;
+ --color-wormhole-2: cyan;
+ --color-wormhole-3: cornflowerblue;
+ --border: solid clamp(0px, 0.5vmin, 5px) var(--color);
+ --border-thin: solid clamp(0px, 0.3vmin, 3px) var(--color);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+.genos-font {
+ font-family: "Genos", sans-serif;
+ font-optical-sizing: auto;
+ font-weight: 400;
+ font-style: normal;
+ font-size: clamp(-100px, 3.5vmin, 35px);
+}
+
+.hidden {
+ display: none !important;
+}
+
+a:link {
+ text-decoration: inherit;
+ color: inherit;
+ cursor: auto;
+}
+
+a:visited {
+ text-decoration: inherit;
+ color: inherit;
+ cursor: auto;
+}
+
+.navigation-menu-wrapper {
+ height: 0;
+ position: relative;
+ margin: 0 auto;
+}
+
+.navigation-menu {
+ margin: auto;
+ width: clamp(-100px, 100vmin, 1000px);
+ display: flex;
+ align-items: center;
+ gap: clamp(-100px, 1vmin, 10px);
+ margin-bottom: 0;
+ margin-top: clamp(-100px, 0.5vmin, 5px);
+ padding-left: clamp(-100px, 4vmin, 40px);
+ opacity: 0.1;
+ transition: opacity 0.3s linear;
+ position: absolute;
+ left: 0;
+ font-size: clamp(-100px, 2vmin, 20px);
+}
+.navigation-menu:hover {
+ opacity: 1;
+}
+.navigation-menu a {
+ color: var(--color);
+ border: var(--border-thin);
+ border-radius: clamp(-100px, 1vmin, 10px);
+ padding: 0 clamp(-100px, 0.6vmin, 6px);
+ cursor: pointer;
+}
+.navigation-menu a:hover {
+ background-color: var(--color-dark);
+}
+.navigation-menu a.active-link {
+ color: var(--glyph-color);
+}
+
+.dropbtn {
+ cursor: pointer;
+}
+
+/* Dropdown button on hover & focus */
+.dropbtn:hover,
+.dropbtn:focus {
+ background-color: var(--color-dark);
+}
+
+/* Dropdown Content (Hidden by Default) */
+.dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: var(--background-color);
+ z-index: 9999;
+}
+
+/* Links inside the dropdown */
+.dropdown-content a {
+ color: var(--color-alt);
+ padding: clamp(-100px, 1vmin, 10px);
+ text-decoration: none;
+ display: block;
+}
+
+/* Change color of dropdown links on hover */
+.dropdown-content a:hover {
+ background-color: var(--color-dark);
+}
+
+/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
+.show {
+ display: block;
+}
+
+body {
+ background-color: var(--background-color);
+ color: var(--color);
+ margin: 0;
+ height: 100vh;
+ display: flex;
+}
+
+.navigation-menu-wrapper {
+ width: clamp(-100px, 98vmin, 980px);
+}
+
+.border {
+ border: var(--border);
+ border-radius: clamp(-100px, 6vmin, 60px);
+ width: clamp(-100px, 98vmin, 980px);
+ height: clamp(-100px, 93vmin, 930px);
+ margin: auto;
+ overflow: hidden;
+ position: relative;
+}
+
+.header {
+ display: flex;
+ gap: clamp(-100px, 1vmin, 10px);
+ margin-left: clamp(-100px, 4.2vmin, 42px);
+ margin-right: clamp(-100px, 8.5vmin, 85px);
+ margin-top: clamp(-100px, 0.8vmin, 8px);
+ position: relative;
+}
+.header .header-2 {
+ color: var(--color-alt);
+ font-size: clamp(-100px, 3.2vmin, 32px);
+ line-height: clamp(-100px, 4.1vmin, 41px);
+ align-content: end;
+}
+.header .header-3 {
+ color: var(--color-alt);
+ font-size: clamp(-100px, 2.9vmin, 29px);
+ line-height: clamp(-100px, 3.9vmin, 39px);
+ align-content: end;
+}
+.header .header-border {
+ border-top: var(--border);
+ position: absolute;
+ width: 100%;
+ bottom: clamp(-100px, 0.6vmin, 6px);
+}
+
+.address-book-glyph {
+ position: relative;
+ float: left;
+ max-width: clamp(-100px, 6vmin, 60px);
+ filter: var(--glyph-color-svg);
+}
+
+.address-counts {
+ position: absolute;
+ font-size: clamp(-100px, 2vmin, 20px);
+ line-height: clamp(-100px, 1.6vmin, 16px);
+ right: clamp(-100px, 1.3vmin, 13px);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ text-align: end;
+ width: clamp(-100px, 12vmin, 120px);
+}
+.address-counts > div {
+ display: flex;
+ justify-content: space-between;
+}
+
+section {
+ margin: 0 clamp(-100px, 1vmin, 10px);
+ position: relative;
+}
+section header {
+ display: flex; /* aligns all child elements (flex items) in a row */
+ margin-bottom: clamp(-100px, 1.5vmin, 15px);
+ line-height: clamp(-100px, 3.5vmin, 35px);
+}
+section header .col {
+ width: clamp(-100px, 7.52vmin, 75.2px);
+ text-align: center;
+ border-bottom: var(--border);
+}
+section header .col.col-1 {
+ width: clamp(-100px, 3.2vmin, 32px);
+ border-bottom: unset;
+}
+section header .col.col-2 {
+ border-image: linear-gradient(to left, var(--color) 50%, transparent 50%) 100% 1;
+}
+section header .col.col-9 {
+ flex: 1;
+ text-align: start;
+}
+section .rows {
+ max-height: clamp(-100px, 66.5vmin, 665px);
+ min-height: clamp(-100px, 66.5vmin, 665px);
+ overflow-y: scroll;
+ scrollbar-color: var(--color);
+ scrollbar-width: auto;
+ position: relative;
+}
+section .row {
+ display: flex; /* aligns all child elements (flex items) in a row */
+ position: relative;
+ margin-bottom: clamp(-100px, 1vmin, 10px);
+ margin-right: clamp(-100px, 1.5vmin, 15px);
+ line-height: clamp(-100px, 3.5vmin, 35px);
+ max-height: clamp(-100px, 9.6vmin, 96px);
+ overflow: hidden;
+ cursor: pointer;
+}
+section .row.danger .glyph,
+section .row.danger .glyph > span,
+section .row.danger .info {
+ border-color: var(--color-danger) !important;
+}
+section .row.danger .small-box {
+ background-color: var(--color-danger) !important;
+}
+section .row.danger .address-book-glyph {
+ filter: var(--glyph-color-danger-svg) !important;
+}
+section .row.fan .info-name {
+ color: var(--color-alt) !important;
+}
+section .row.offline .status .glyph-color {
+ color: var(--color-danger) !important;
+}
+section .row .col {
+ width: clamp(-100px, 7.4vmin, 74px);
+ text-align: center;
+ border-bottom: var(--border);
+}
+section .row .col.box {
+ flex: 0;
+ align-content: end;
+ padding-right: clamp(-100px, 0.2vmin, 2px);
+ border: unset;
+}
+section .row .col.box div {
+ width: clamp(-100px, 3vmin, 30px);
+ line-height: clamp(-100px, 2vmin, 20px);
+ text-align: center;
+ background-color: var(--color);
+ color: var(--background-color);
+ padding-bottom: clamp(-100px, 1.5vmin, 15px);
+}
+section .row .col.glyph {
+ border: var(--border);
+ border-right: unset;
+ flex: 0;
+ min-width: clamp(-100px, 7vmin, 70px);
+ max-width: clamp(-100px, 7vmin, 70px);
+ font-size: clamp(-100px, 1.8vmin, 18px);
+ white-space: nowrap;
+ color: var(--glyph-color);
+ display: flex;
+ flex-direction: column;
+}
+section .row .col.glyph > img {
+ margin: auto;
+ text-shadow: unset;
+ height: clamp(-100px, 6vmin, 60px);
+}
+section .row .col.glyph > span {
+ text-align: center;
+ border-top: var(--border);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ line-height: clamp(-100px, 2.3vmin, 23px);
+}
+section .row .col.info {
+ flex: 1;
+ text-align: start;
+ border-bottom: unset;
+ border-top: var(--border);
+ border-left: var(--border);
+ font-size: clamp(-100px, 2.75vmin, 27.5px);
+ padding-left: clamp(-100px, 0.5vmin, 5px);
+ line-height: clamp(-100px, 2.3vmin, 23px);
+}
+section .row .col.info .info-title {
+ display: flex;
+ gap: clamp(-100px, 1vmin, 10px);
+}
+section .row .col.info .info-title .info-name {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+section .row .col.info .info-body {
+ display: flex;
+ gap: clamp(-100px, 2vmin, 20px);
+}
+section .row .col.info .info-body .info-a {
+ flex: 1;
+ font-size: clamp(-100px, 2vmin, 20px);
+ line-height: clamp(-100px, 1.7vmin, 17px);
+ margin-left: clamp(-100px, 1vmin, 10px);
+}
+section .row .col.info .info-body .info-b {
+ flex: 1;
+ font-size: clamp(-100px, 2vmin, 20px);
+ line-height: clamp(-100px, 1.7vmin, 17px);
+}
+section .row .col.info .info-body .info-b > div {
+ display: flex;
+ gap: clamp(-100px, 0.69vmin, 6.9px);
+}
+section .row .col.info .info-body .info-b .glyph-color {
+ color: var(--glyph-color);
+}
+section .row .col.info .info-body .info-b .alt-color {
+ color: var(--color-alt);
+}
+section .row::after {
+ content: "▼";
+ width: calc(100% - clamp(-100px, 1vmin, 10px));
+ height: calc(100% - clamp(-100px, 0.7vmin, 7px));
+ border: var(--border);
+ border-color: var(--color-good);
+ color: var(--color-good);
+ position: absolute;
+ font-size: clamp(-100px, 2.25vmin, 22.5px);
+ line-height: clamp(-100px, 2.25vmin, 22.5px);
+ padding-left: clamp(-100px, 0.2vmin, 2px);
+ pointer-events: none;
+ opacity: 0;
+}
+section .row:hover::after {
+ opacity: 1;
+}
+
+.table-footer {
+ border: var(--border);
+ border-left: unset;
+ border-right: unset;
+ color: var(--color-alt);
+ font-size: clamp(-100px, 2.5vmin, 25px);
+ padding-left: clamp(-100px, 0.5vmin, 5px);
+ line-height: clamp(-100px, 2.5vmin, 25px);
+}
+
+.footer {
+ display: flex;
+ border-bottom: var(--border);
+}
+.footer .footer-left {
+ flex: 1 0 auto;
+ max-width: clamp(-100px, 72vmin, 720px);
+ min-width: clamp(-100px, 72vmin, 720px);
+}
+.footer .footer-left > .footer-text {
+ border: var(--border);
+ border-left: unset;
+ border-bottom: unset;
+ margin-right: clamp(-100px, 0.5vmin, 5px);
+ font-size: clamp(-100px, 2vmin, 20px);
+ line-height: clamp(-100px, 1.6vmin, 16px);
+ padding-left: clamp(-100px, 0.5vmin, 5px);
+ height: clamp(-100px, 4vmin, 40px);
+ overflow: hidden;
+}
+.footer .footer-left > .footer-text div {
+ line-height: clamp(-100px, 1.6vmin, 16px);
+ animation: footer-text-anim 15s steps(1) forwards;
+}
+.footer .footer-right {
+ flex: 1 1 auto;
+ font-size: clamp(-100px, 1.6vmin, 16px);
+ color: var(--glyph-color);
+ max-height: clamp(-100px, 8vmin, 80px);
+ overflow: hidden;
+}
+
+@keyframes footer-text-anim {
+ 0% {
+ transform: translateY(0%);
+ }
+ 10% {
+ transform: translateY(-10%);
+ }
+ 20% {
+ transform: translateY(-20%);
+ }
+ 30% {
+ transform: translateY(-30%);
+ }
+ 40% {
+ transform: translateY(-40%);
+ }
+ 50% {
+ transform: translateY(-50%);
+ }
+ 60% {
+ transform: translateY(-60%);
+ }
+ 70% {
+ transform: translateY(-70%);
+ }
+ 80% {
+ transform: translateY(-80%);
+ }
+ 100% {
+ transform: translateY(-80%);
+ } /* Hold final position */
+}
+.footer-buttons {
+ display: flex;
+ font-size: clamp(-100px, 2.5vmin, 25px);
+ text-align: center;
+ margin: clamp(-100px, 0.5vmin, 5px);
+ gap: clamp(-100px, 0.5vmin, 5px);
+}
+.footer-buttons a {
+ flex: 1;
+ border: var(--border);
+ border-radius: clamp(-100px, 0.5vmin, 5px);
+ line-height: clamp(-100px, 2.5vmin, 25px);
+ cursor: pointer;
+ color: var(--color);
+ text-decoration: unset !important;
+}
+.footer-buttons a:hover {
+ color: var(--color-alt) !important;
+}
+.footer-buttons a.active-button {
+ color: var(--glyph-color) !important;
+}
+
+.scrolling-section-wrapper {
+ width: clamp(-100px, 87vmin, 870px);
+ border: var(--border-thin);
+ margin: clamp(-100px, 0.5vmin, 5px) clamp(-100px, 5vmin, 50px);
+ overflow: hidden;
+}
+
+.scrolling-section {
+ display: flex;
+ flex-wrap: nowrap;
+ width: max-content;
+ font-size: clamp(-100px, 2vmin, 20px);
+ animation: move 40s infinite linear;
+ line-height: clamp(-100px, 1.5vmin, 15px);
+}
+
+@keyframes move {
+ to {
+ transform: translateX(-50%);
+ }
+}
+.scrolling-sectionY {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ line-height: clamp(-100px, 1.4vmin, 14px);
+ font-size: clamp(-100px, 2vmin, 20px);
+ animation: moveY 1.5s infinite linear;
+}
+
+@keyframes moveY {
+ to {
+ transform: translateY(-50%);
+ }
+}
+
+/*# sourceMappingURL=address_book.css.map */
diff --git a/web/retro/css/crt.css b/web/retro/css/crt.css
new file mode 100644
index 0000000..fa8f931
--- /dev/null
+++ b/web/retro/css/crt.css
@@ -0,0 +1,232 @@
+:root {
+ --screen-door-small: 100% 2px, 3px 100%;
+ --screen-door-medium: 100% 4px, 6px 100%;
+ --screen-door-large: 100% 6px, 9px 100%;
+}
+
+/* CRT CSS CODE: https://aleclownes.com/2017/02/01/crt-display.html */
+@keyframes flicker {
+ 0% {
+ opacity: 0.27861;
+ }
+ 5% {
+ opacity: 0.34769;
+ }
+ 10% {
+ opacity: 0.23604;
+ }
+ 15% {
+ opacity: 0.90626;
+ }
+ 20% {
+ opacity: 0.18128;
+ }
+ 25% {
+ opacity: 0.83891;
+ }
+ 30% {
+ opacity: 0.65583;
+ }
+ 35% {
+ opacity: 0.67807;
+ }
+ 40% {
+ opacity: 0.26559;
+ }
+ 45% {
+ opacity: 0.84693;
+ }
+ 50% {
+ opacity: 0.96019;
+ }
+ 55% {
+ opacity: 0.08594;
+ }
+ 60% {
+ opacity: 0.20313;
+ }
+ 65% {
+ opacity: 0.71988;
+ }
+ 70% {
+ opacity: 0.53455;
+ }
+ 75% {
+ opacity: 0.37288;
+ }
+ 80% {
+ opacity: 0.71428;
+ }
+ 85% {
+ opacity: 0.70419;
+ }
+ 90% {
+ opacity: 0.7003;
+ }
+ 95% {
+ opacity: 0.36108;
+ }
+ 100% {
+ opacity: 0.24387;
+ }
+}
+@keyframes textShadow {
+ 0% {
+ text-shadow: 0.4389924193300864px 0 1px rgba(0, 30, 255, 0.5),
+ -0.4389924193300864px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 5% {
+ text-shadow: 2.7928974010788217px 0 1px rgba(0, 30, 255, 0.5),
+ -2.7928974010788217px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 10% {
+ text-shadow: 0.02956275843481219px 0 1px rgba(0, 30, 255, 0.5),
+ -0.02956275843481219px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 15% {
+ text-shadow: 0.40218538552878136px 0 1px rgba(0, 30, 255, 0.5),
+ -0.40218538552878136px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 20% {
+ text-shadow: 3.4794037899852017px 0 1px rgba(0, 30, 255, 0.5),
+ -3.4794037899852017px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 25% {
+ text-shadow: 1.6125630401149584px 0 1px rgba(0, 30, 255, 0.5),
+ -1.6125630401149584px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 30% {
+ text-shadow: 0.7015590085143956px 0 1px rgba(0, 30, 255, 0.5),
+ -0.7015590085143956px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 35% {
+ text-shadow: 3.896914047650351px 0 1px rgba(0, 30, 255, 0.5),
+ -3.896914047650351px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 40% {
+ text-shadow: 3.870905614848819px 0 1px rgba(0, 30, 255, 0.5),
+ -3.870905614848819px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 45% {
+ text-shadow: 2.231056963361899px 0 1px rgba(0, 30, 255, 0.5),
+ -2.231056963361899px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 50% {
+ text-shadow: 0.08084290417898504px 0 1px rgba(0, 30, 255, 0.5),
+ -0.08084290417898504px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 55% {
+ text-shadow: 2.3758461067427543px 0 1px rgba(0, 30, 255, 0.5),
+ -2.3758461067427543px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 60% {
+ text-shadow: 2.202193051050636px 0 1px rgba(0, 30, 255, 0.5),
+ -2.202193051050636px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 65% {
+ text-shadow: 2.8638780614874975px 0 1px rgba(0, 30, 255, 0.5),
+ -2.8638780614874975px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 70% {
+ text-shadow: 0.48874025155497314px 0 1px rgba(0, 30, 255, 0.5),
+ -0.48874025155497314px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 75% {
+ text-shadow: 1.8948491305757957px 0 1px rgba(0, 30, 255, 0.5),
+ -1.8948491305757957px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 80% {
+ text-shadow: 0.0833037308038857px 0 1px rgba(0, 30, 255, 0.5),
+ -0.0833037308038857px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 85% {
+ text-shadow: 0.09769827255241735px 0 1px rgba(0, 30, 255, 0.5),
+ -0.09769827255241735px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 90% {
+ text-shadow: 3.443339761481782px 0 1px rgba(0, 30, 255, 0.5),
+ -3.443339761481782px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 95% {
+ text-shadow: 2.1841838852799786px 0 1px rgba(0, 30, 255, 0.5),
+ -2.1841838852799786px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+ 100% {
+ text-shadow: 2.6208764473832513px 0 1px rgba(0, 30, 255, 0.5),
+ -2.6208764473832513px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px;
+ }
+}
+.crt::after {
+ content: " ";
+ display: block;
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(18, 16, 16, 0.1);
+ opacity: 0;
+ z-index: 2;
+ pointer-events: none;
+}
+.crt::before {
+ content: " ";
+ display: block;
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: linear-gradient(
+ rgba(18, 16, 16, 0) 50%,
+ rgba(0, 0, 0, 0.25) 50%
+ ),
+ linear-gradient(
+ 90deg,
+ rgba(255, 0, 0, 0.06),
+ rgba(0, 255, 0, 0.02),
+ rgba(0, 0, 255, 0.06)
+ );
+ z-index: 2;
+ background-size: var(--screen-door-medium);
+ pointer-events: none;
+}
+.crt {
+ /* ENABLE THIS IF YOU WANT TO MAKE YOUR COMPUTER HURT */
+ /* Firefox handles it better than Chrome */
+ /* Looks super dope tho. */
+ /* animation: textShadow 3.5s infinite; */
+
+
+ filter: contrast(1.5) brightness(1.1) saturate(1.1);
+}
+
+.crt.flicker::after {
+ animation: flicker 0.15s infinite;
+}
+
+@keyframes crt-scanline {
+ 0% {
+ top: -5%;
+ }
+ 100% {
+ top: 125%;
+ }
+}
+
+.crt-distortion {
+ position: absolute;
+ top: -5%;
+ left: 0;
+ width: 100%;
+ height: 2vmin; /* Small strip for tearing effect */
+ background: rgba(255, 255, 255, 0.1); /* Very faint */
+ backdrop-filter: blur(1px);
+ filter: blur(1vmin);
+ pointer-events: none;
+ z-index: 100;
+}
+
+.scanline-animation {
+ animation: crt-scanline 8s linear 1;
+}
\ No newline at end of file
diff --git a/web/retro/css/dial.css b/web/retro/css/dial.css
new file mode 100644
index 0000000..fb72787
--- /dev/null
+++ b/web/retro/css/dial.css
@@ -0,0 +1,667 @@
+:root {
+ /*! UPDATE COLORS HERE !!! -------------------------------------------------------------------------------------- */
+ --color: #37bfde;
+ --color-dark: #4a7297;
+ --color-danger: #c70036;
+ --color-good: #07ff0b;
+ --color-alt: white;
+ --background-color: #020f25;
+ --glyph-color: #fffea5;
+ /*! Calculate glyph-color-svg here: https://codepen.io/sosuke/pen/Pjoqqp */
+ --glyph-color-svg: invert(100%) sepia(21%) saturate(5367%) hue-rotate(312deg)
+ brightness(111%) contrast(103%);
+ --glyph-color-danger-svg: invert(16%) sepia(84%) saturate(3541%)
+ hue-rotate(331deg) brightness(81%) contrast(117%);
+ --color-wormhole-danger-1: yellow;
+ --color-wormhole-danger-2: orange;
+ --color-wormhole-danger-3: red;
+ --color-wormhole-1: royalblue;
+ --color-wormhole-2: cyan;
+ --color-wormhole-3: cornflowerblue;
+ --border: solid clamp(0px, 0.5vmin, 5px) var(--color);
+ --border-thin: solid clamp(0px, 0.3vmin, 3px) var(--color);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+.genos-font {
+ font-family: "Genos", sans-serif;
+ font-optical-sizing: auto;
+ font-weight: 400;
+ font-style: normal;
+ font-size: clamp(-100px, 3.5vmin, 35px);
+}
+
+.hidden {
+ display: none !important;
+}
+
+a:link {
+ text-decoration: inherit;
+ color: inherit;
+ cursor: auto;
+}
+
+a:visited {
+ text-decoration: inherit;
+ color: inherit;
+ cursor: auto;
+}
+
+.navigation-menu-wrapper {
+ height: 0;
+ position: relative;
+ margin: 0 auto;
+}
+
+.navigation-menu {
+ margin: auto;
+ width: clamp(-100px, 100vmin, 1000px);
+ display: flex;
+ align-items: center;
+ gap: clamp(-100px, 1vmin, 10px);
+ margin-bottom: 0;
+ margin-top: clamp(-100px, 0.5vmin, 5px);
+ padding-left: clamp(-100px, 4vmin, 40px);
+ opacity: 0.1;
+ transition: opacity 0.3s linear;
+ position: absolute;
+ left: 0;
+ font-size: clamp(-100px, 2vmin, 20px);
+}
+.navigation-menu:hover {
+ opacity: 1;
+}
+.navigation-menu a {
+ color: var(--color);
+ border: var(--border-thin);
+ border-radius: clamp(-100px, 1vmin, 10px);
+ padding: 0 clamp(-100px, 0.6vmin, 6px);
+ cursor: pointer;
+}
+.navigation-menu a:hover {
+ background-color: var(--color-dark);
+}
+.navigation-menu a.active-link {
+ color: var(--glyph-color);
+}
+
+.dropbtn {
+ cursor: pointer;
+}
+
+/* Dropdown button on hover & focus */
+.dropbtn:hover,
+.dropbtn:focus {
+ background-color: var(--color-dark);
+}
+
+/* Dropdown Content (Hidden by Default) */
+.dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: var(--background-color);
+ z-index: 9999;
+}
+
+/* Links inside the dropdown */
+.dropdown-content a {
+ color: var(--color-alt);
+ padding: clamp(-100px, 1vmin, 10px);
+ text-decoration: none;
+ display: block;
+}
+
+/* Change color of dropdown links on hover */
+.dropdown-content a:hover {
+ background-color: var(--color-dark);
+}
+
+/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
+.show {
+ display: block;
+}
+
+.reference {
+ background-image: url("../../references/gate-ref.jpg");
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ background-size: cover;
+ opacity: 0.5;
+}
+
+body {
+ background-color: var(--background-color);
+ color: var(--color);
+ margin: 0;
+ height: 100vh;
+ display: flex;
+}
+
+.navigation-menu-wrapper {
+ width: clamp(-100px, 98vmin, 980px);
+}
+
+.border {
+ border: var(--border);
+ border-radius: clamp(-100px, 3vmin, 30px);
+ width: clamp(-100px, 98vmin, 980px);
+ height: clamp(-100px, 72vmin, 720px);
+ margin: auto;
+ overflow: hidden;
+ position: relative;
+}
+
+.crosshair {
+ display: flex;
+ width: clamp(-100px, 45.1vmin, 451px);
+ align-items: center;
+ justify-content: center;
+}
+.crosshair div {
+ text-align: center;
+ align-content: center;
+ position: absolute;
+ color: var(--glyph-color);
+}
+
+.border.idle .crosshair .blink {
+ width: clamp(-100px, 16vmin, 160px);
+ height: clamp(-100px, 16vmin, 160px);
+ position: absolute;
+ background: radial-gradient(circle, var(--color-danger) 0%, transparent 70%);
+ animation: blink 3s infinite ease;
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 0;
+ }
+ 40% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 60% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+.chevron-box {
+ border: var(--border-thin);
+ width: clamp(-100px, 8.6vmin, 86px);
+ height: clamp(-100px, 6vmin, 60px);
+ left: clamp(-100px, 81.4vmin, 814px);
+ position: absolute;
+ border-radius: clamp(-100px, 1.5vmin, 15px) 0 0;
+ overflow: hidden;
+ text-shadow: unset !important;
+}
+.chevron-box.b1 {
+ top: clamp(-100px, 11vmin, 110px);
+}
+.chevron-box.b2 {
+ top: clamp(-100px, 19vmin, 190px);
+}
+.chevron-box.b3 {
+ top: clamp(-100px, 27vmin, 270px);
+}
+.chevron-box.b4 {
+ top: clamp(-100px, 35vmin, 350px);
+}
+.chevron-box.b5 {
+ top: clamp(-100px, 43vmin, 430px);
+}
+.chevron-box.b6 {
+ top: clamp(-100px, 51vmin, 510px);
+}
+.chevron-box.b7 {
+ top: clamp(-100px, 59vmin, 590px);
+}
+
+.chevron-box-labels {
+ position: absolute;
+ top: clamp(-100px, 14.2vmin, 142px);
+ right: clamp(-100px, 16.7vmin, 167px);
+ display: flex;
+ flex-direction: column;
+ color: var(--color-alt);
+}
+.chevron-box-labels > span {
+ margin-bottom: clamp(-100px, 3.8vmin, 38px);
+ line-height: clamp(-100px, 4.2vmin, 42px);
+ font-size: clamp(-100px, 2.6vmin, 26px);
+}
+
+.lst-code-1,
+.lst-code-2 {
+ position: absolute;
+ bottom: clamp(-100px, 15.8vmin, 158px);
+ color: var(--color-alt);
+ font-size: clamp(-100px, 1.8vmin, 18px);
+}
+.lst-code-1 span,
+.lst-code-2 span {
+ font-size: clamp(-100px, 2.5vmin, 25px);
+}
+
+.lst-code-1 {
+ left: clamp(-100px, 19.6vmin, 196px);
+}
+
+.lst-code-2 {
+ left: clamp(-100px, 67.6vmin, 676px);
+}
+
+.auth-code-label {
+ position: absolute;
+ bottom: clamp(-100px, 0.2vmin, 2px);
+ left: clamp(-100px, 17.7vmin, 177px);
+ color: var(--color-alt);
+ font-size: clamp(-100px, 2vmin, 20px);
+}
+
+.auth-code {
+ position: absolute;
+ bottom: clamp(-100px, 0.4vmin, 4px);
+ left: clamp(-100px, 37vmin, 370px);
+ color: var(--color-alt);
+ font-size: clamp(-100px, 2.3vmin, 23px);
+ display: flex;
+}
+.auth-code div {
+ width: clamp(-100px, 2.3vmin, 23px);
+ text-align: center;
+ border-bottom: var(--border-thin);
+ border-right: var(--border-thin);
+ border-color: var(--color-dark);
+}
+.auth-code div:nth-child(1) {
+ padding-left: clamp(-100px, 3.3vmin, 33px);
+ width: clamp(-100px, 6vmin, 60px);
+}
+.auth-code div:nth-child(7) {
+ padding: 0 clamp(-100px, 1vmin, 10px);
+ width: clamp(-100px, 5vmin, 50px);
+}
+.auth-code div:nth-child(15) {
+ padding-right: clamp(-100px, 0.7vmin, 7px);
+ width: clamp(-100px, 3vmin, 30px);
+}
+
+.system {
+ position: absolute;
+ bottom: clamp(-100px, 0.8vmin, 8px);
+ left: clamp(-100px, 78.8vmin, 788px);
+ color: var(--color-alt);
+ font-size: clamp(-100px, 1.1vmin, 11px);
+ display: flex;
+ flex-direction: column;
+}
+
+.info-box {
+ color: var(--color-danger);
+ font-weight: bold;
+ position: absolute;
+ bottom: clamp(-100px, 4.2vmin, 42px);
+ left: clamp(-100px, 20.9vmin, 209px);
+ border: var(--border);
+ width: clamp(-100px, 53.7vmin, 537px);
+ height: clamp(-100px, 7.9vmin, 79px);
+ text-align: center;
+ font-size: clamp(-100px, 9vmin, 90px);
+ line-height: clamp(-100px, 6vmin, 60px);
+ word-spacing: clamp(-100px, 14vmin, 140px);
+ letter-spacing: clamp(-100px, 1.5vmin, 15px);
+ padding-left: clamp(-100px, 1.3vmin, 13px);
+ animation: pulsingTextDanger 3.4s linear infinite;
+}
+
+.sidebar {
+ border: var(--border);
+ border-left: unset;
+ border-radius: 0 clamp(-100px, 2vmin, 20px) clamp(-100px, 2vmin, 20px) 0;
+ position: absolute;
+ top: clamp(-100px, 10.8vmin, 108px);
+ left: clamp(-100px, 0vmin, 0px);
+ width: clamp(-100px, 17.9vmin, 179px);
+ height: clamp(-100px, 56.1vmin, 561px);
+}
+.sidebar .keyboard {
+ width: clamp(-100px, 16vmin, 160px);
+ height: clamp(-100px, 39vmin, 390px);
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: start;
+ align-content: start;
+ margin: auto;
+}
+.sidebar .keyboard div {
+ width: clamp(-100px, 3.9vmin, 39px);
+ height: clamp(-100px, 3.9vmin, 39px);
+ cursor: pointer;
+ border: var(--border-thin);
+ border-color: rgba(0, 0, 0, 0);
+ border-radius: clamp(-100px, 1vmin, 10px);
+}
+.sidebar .keyboard div:hover {
+ border-color: var(--color-dark);
+}
+.sidebar .keyboard div.disabled {
+ opacity: 0.4;
+}
+.sidebar .keyboard div img {
+ width: clamp(-100px, 3.6vmin, 36px);
+ height: clamp(-100px, 3.6vmin, 36px);
+ filter: var(--glyph-color-svg);
+}
+.sidebar .status {
+ color: var(--color-alt);
+ font-size: clamp(-100px, 1.1vmin, 11px);
+ padding-left: clamp(-100px, 1.5vmin, 15px);
+}
+
+.timer {
+ position: absolute;
+ bottom: clamp(-100px, 0.7vmin, 7px);
+ left: clamp(-100px, 1.4vmin, 14px);
+ font-size: clamp(-100px, 4vmin, 40px);
+ border: var(--border-thin);
+ width: clamp(-100px, 15vmin, 150px);
+ height: clamp(-100px, 3.2vmin, 32px);
+ text-align: center;
+ line-height: clamp(-100px, 2.3vmin, 23px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ animation: pulsingText 3s linear infinite;
+}
+
+.gate-name {
+ position: absolute;
+ top: clamp(-100px, 1.7vmin, 17px);
+ left: clamp(-100px, 10.9vmin, 109px);
+ font-size: clamp(-100px, 3.5vmin, 35px);
+ border: var(--border-thin);
+ border-color: var(--color-dark);
+ width: clamp(-100px, 48vmin, 480px);
+ height: clamp(-100px, 4.5vmin, 45px);
+ line-height: clamp(-100px, 4vmin, 40px);
+ padding-left: clamp(-100px, 0.6vmin, 6px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.destination-box {
+ position: absolute;
+ top: clamp(-100px, 1.7vmin, 17px);
+ right: clamp(-100px, 1.1vmin, 11px);
+ font-size: clamp(-100px, 1.9vmin, 19px);
+ border: var(--border-thin);
+ border-color: var(--color-dark);
+ width: clamp(-100px, 36vmin, 360px);
+ height: clamp(-100px, 4.5vmin, 45px);
+ line-height: clamp(-100px, 6.2vmin, 62px);
+ text-align: center;
+ color: white;
+}
+
+.destination {
+ position: absolute;
+ top: clamp(-100px, 1.7vmin, 17px);
+ right: clamp(-100px, 1.2vmin, 12px);
+ font-size: clamp(-100px, 3.5vmin, 35px);
+ width: clamp(-100px, 36.3vmin, 363px);
+ height: clamp(-100px, 3.2vmin, 32px);
+ line-height: clamp(-100px, 3vmin, 30px);
+ text-align: center;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.destination-glyphs {
+ position: absolute;
+ top: clamp(-100px, 1.7vmin, 17px);
+ right: clamp(-100px, 1.2vmin, 12px);
+ width: clamp(-100px, 36.3vmin, 363px);
+ height: clamp(-100px, 3.2vmin, 32px);
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+}
+.destination-glyphs img {
+ width: clamp(-100px, 2.5vmin, 25px);
+ filter: var(--glyph-color-svg);
+}
+
+.chevron-states {
+ font-size: clamp(-100px, 2vmin, 20px);
+ line-height: clamp(-100px, 1.6vmin, 16px);
+ border-collapse: collapse;
+ table-layout: fixed;
+ width: clamp(-100px, 18vmin, 180px);
+}
+.chevron-states .chevron-open,
+.chevron-states .chevron-locked {
+ width: clamp(-100px, 5.2vmin, 52px);
+}
+.chevron-states .chevron-open > div,
+.chevron-states .chevron-locked > div {
+ border: clamp(-100px, 0.2vmin, 2px) solid var(--color-dark);
+ width: clamp(-100px, 3.5vmin, 35px);
+ height: clamp(-100px, 1vmin, 10px);
+ background-color: rgba(0, 0, 0, 0);
+ transition: background-color 0.5s ease;
+}
+.chevron-states .chevron-open > div {
+ background-color: var(--color-danger);
+}
+.chevron-states .chevron-number {
+ color: var(--color-alt);
+ width: clamp(-100px, 2vmin, 20px);
+}
+.chevron-states .chevron-state {
+ color: var(--color-good);
+ font-size: clamp(-100px, 1.3vmin, 13px);
+ visibility: hidden;
+}
+.chevron-states tr {
+ height: clamp(-100px, 2.1vmin, 21px);
+}
+.chevron-states td {
+ padding-left: clamp(-100px, 1.6vmin, 16px);
+}
+.chevron-states tr.locked .chevron-open > div {
+ background-color: rgba(0, 0, 0, 0);
+}
+.chevron-states tr.locked .chevron-locked > div {
+ background-color: var(--color-good);
+}
+.chevron-states tr.locked .chevron-state {
+ visibility: visible;
+}
+
+.dial-append {
+ position: relative;
+ top: 0;
+ left: 0;
+}
+
+div.shield {
+ position: absolute;
+ top: clamp(-100px, 1.5vmin, 15px);
+ left: clamp(-100px, 1.5vmin, 15px);
+ width: clamp(-100px, 8.8vmin, 88px);
+ height: clamp(-100px, 8.8vmin, 88px);
+ background-image: url("../images/shield.gif");
+ background-size: contain;
+ background-blend-mode: overlay;
+ filter: brightness(2) contrast(0.5);
+ border: var(--border);
+ border-right: unset;
+ border-radius: clamp(-100px, 2vmin, 20px) 0 0 clamp(-100px, 1vmin, 10px);
+}
+
+div.shield-buddy {
+ position: absolute;
+ top: clamp(-100px, 2.3vmin, 23px);
+ left: clamp(-100px, 9.5vmin, 95px);
+ width: clamp(-100px, 8vmin, 80px);
+ height: clamp(-100px, 8vmin, 80px);
+ border-bottom: var(--border);
+ text-shadow: unset !important;
+}
+
+div.clip-1 {
+ border-color: var(--color-danger);
+ clip-path: inset(0 100% 0 0); /* Hidden initially (right side at 100%) */
+ transition: clip-path 0.8s linear;
+}
+
+div.clip-2 {
+ border-color: var(--color-danger);
+ clip-path: inset(0 0 0 100%); /* Hidden initially (right side at 100%) */
+ transition: clip-path 0.8s linear;
+}
+
+div.locked {
+ clip-path: inset(0 0 0 0);
+}
+
+div:not(.active) .ring-1 circle {
+ fill: none !important;
+}
+
+img.glyph {
+ text-shadow: unset;
+ filter: var(--glyph-color-svg);
+ top: clamp(-100px, 19vmin, 190px);
+ left: clamp(-100px, 33.1vmin, 331px);
+ height: clamp(-100px, 30vmin, 300px);
+ position: absolute;
+ transition-property: top, left, height;
+ transition-duration: 0.8s;
+ transition-timing-function: ease;
+ animation: revealAnimation 0.8s forwards ease;
+}
+img.glyph.locked {
+ left: clamp(-100px, 82.2vmin, 822px);
+ height: clamp(-100px, 6.6vmin, 66px);
+}
+img.glyph.locked.g1 {
+ top: clamp(-100px, 10.7vmin, 107px);
+}
+img.glyph.locked.g2 {
+ top: clamp(-100px, 18.7vmin, 187px);
+}
+img.glyph.locked.g3 {
+ top: clamp(-100px, 26.7vmin, 267px);
+}
+img.glyph.locked.g4 {
+ top: clamp(-100px, 34.7vmin, 347px);
+}
+img.glyph.locked.g5 {
+ top: clamp(-100px, 42.7vmin, 427px);
+}
+img.glyph.locked.g6 {
+ top: clamp(-100px, 50.7vmin, 507px);
+}
+img.glyph.locked.g7 {
+ top: clamp(-100px, 58.7vmin, 587px);
+}
+img.glyph.locked.g8 {
+ top: clamp(-100px, 80vmin, 800px);
+}
+img.glyph.locked.g9 {
+ top: clamp(-100px, 80vmin, 800px);
+}
+img.glyph.blur {
+ filter: blur(clamp(-100px, 1vmin, 10px)) var(--glyph-color-svg);
+}
+
+div.chevron-link {
+ position: absolute;
+ left: clamp(-100px, 0vmin, 0px);
+ height: clamp(-100px, 73vmin, 730px);
+ pointer-events: none;
+ text-shadow: unset !important;
+ animation: none;
+}
+
+img.gate,
+div.gate {
+ position: absolute;
+ left: clamp(-100px, 25.6vmin, 256px);
+ top: clamp(-100px, 12.9vmin, 129px);
+ height: clamp(-100px, 44.8vmin, 448px);
+}
+
+@keyframes revealAnimation {
+ from {
+ transform: scale(0.1);
+ }
+ to {
+ transform: scale(1);
+ }
+}
+@keyframes pulsingText {
+ 0% {
+ color: rgb(from var(--color) r g b/100%);
+ }
+ 50% {
+ color: rgb(from var(--color) r g b/100%);
+ }
+ 80% {
+ color: rgb(from var(--color) r g b/40%);
+ }
+ 95% {
+ color: rgb(from var(--color) r g b/60%);
+ }
+ 100% {
+ color: rgb(from var(--color) r g b/100%);
+ }
+}
+@keyframes pulsingTextDanger {
+ 0% {
+ color: rgb(from var(--color-danger) r g b/100%);
+ }
+ 50% {
+ color: rgb(from var(--color-danger) r g b/100%);
+ }
+ 80% {
+ color: rgb(from var(--color-danger) r g b/60%);
+ }
+ 95% {
+ color: rgb(from var(--color-danger) r g b/80%);
+ }
+ 100% {
+ color: rgb(from var(--color-danger) r g b/100%);
+ }
+}
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+.rotating {
+ animation: spin 8s ease-in infinite;
+}
+
+.slow-rotate {
+ transition: transform 0.4s ease-out;
+}
+
+/*# sourceMappingURL=dial.css.map */
diff --git a/web/retro/dial.html b/web/retro/dial.html
new file mode 100644
index 0000000..5cbd6b0
--- /dev/null
+++ b/web/retro/dial.html
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+
+
+ Stargate Command
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
DESTINATION
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+
+
+
LST CODE # 1
+
LST CODE # 2
+
+
AUTHORIZATION CODE:
+
+
7
+
7
+
8
+
9
+
2
+
7
+
-
+
5
+
7
+
8
+
9
+
2
+
3
+
8
+
7
+
+
+
+ USER: SGT. W HARRIMANSYS: NOMINAL
+
+
+
+
+
+
+
0:00
+
+

+
+
+
+
+
+
+
+
+
+
diff --git a/web/retro/images/dhd.svg b/web/retro/images/dhd.svg
new file mode 100644
index 0000000..d1b6653
--- /dev/null
+++ b/web/retro/images/dhd.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/web/retro/images/shield.gif b/web/retro/images/shield.gif
new file mode 100644
index 0000000..afb4659
Binary files /dev/null and b/web/retro/images/shield.gif differ
diff --git a/web/retro/js/address_book.js b/web/retro/js/address_book.js
new file mode 100644
index 0000000..d0e9f86
--- /dev/null
+++ b/web/retro/js/address_book.js
@@ -0,0 +1,424 @@
+const scrollingDiv = document.getElementById('scrollingDiv');
+const tableRowTemplate = document.getElementById('tableRow');
+const tableBody = document.getElementById('tableBody');
+const standardCounts = document.querySelector('.standard-count');
+const fanCounts = document.querySelector('.fan-count');
+
+addresses = [];
+symbols = [];
+
+fetchData();
+
+function updateIp(ip) {
+ const parts = ip.split('.').map(Number);
+
+ return parts.join('·');
+}
+
+function hash(str) {
+ let hash = 0,
+ i,
+ chr;
+ if (str.length === 0) return hash;
+ for (i = 0; i < str.length; i++) {
+ chr = str.charCodeAt(i);
+ hash = (hash << 5) - hash + chr;
+ hash |= 0; // Convert to 32bit integer
+ }
+ return hash;
+}
+
+const randomText = ['E
M', 'P
Q', 'G
X', 'T
V', 'I
X'];
+
+async function fetchData() {
+ try {
+ const response = await fetch('/stargate/get/address_book?type=all');
+ const data = await response.json();
+ standardCounts.textContent = data.summary.standard;
+ fanCounts.textContent = data.summary.fan;
+ addresses = Object.values(data['address_book']);
+
+ const responseSymbols = await fetch('/stargate/get/symbols_all');
+ symbols = await responseSymbols.json();
+
+ parseData();
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ }
+}
+
+function parseData() {
+ addresses.forEach(address => {
+ const aTag = document.createElement('a');
+ aTag.setAttribute('href', `/stargate/dial?address=${address.gate_address.join('-')}`);
+ address.htmlData = aTag;
+
+ const newRow = tableRowTemplate.cloneNode(true);
+ aTag.appendChild(newRow);
+ newRow.removeAttribute('id');
+ newRow.classList.remove('hidden');
+
+ let keyboardAddress = '';
+ let hasUnknownGlyph = false;
+
+ address['gate_address'].forEach((glyph, i) => {
+ const symbol = symbols.find(x => x['index'] === glyph);
+
+ if (!symbol || symbol['keyboard_mapping'] === false) {
+ keyboardAddress += '?';
+ hasUnknownGlyph = true;
+ } else {
+ keyboardAddress += symbol['keyboard_mapping'];
+ }
+
+ if (i < 7) {
+ newRow.querySelector(`.glyph-name-${i + 1}`).textContent =
+ symbol?.['name'] ?? 'Unknown';
+
+ const imgElement = newRow.querySelector(`.glyph-${i + 1}`);
+ imgElement.src = ''; // Empty it temporarily
+ imgElement.src = '..' + symbol?.['imageSrc']; // Set it again to force a reload
+ }
+ });
+
+ const randomNumber = Math.floor(Math.random() * 5);
+ newRow.querySelector('.small-box').innerHTML = randomText[randomNumber];
+
+ newRow.querySelector(
+ `.info-name`,
+ ).textContent = `${address['name']} # ${keyboardAddress}8`;
+
+ if (
+ address['is_black_hole'] ||
+ hasUnknownGlyph ||
+ address['gate_address'].length > 6
+ ) {
+ newRow.classList.add('danger');
+ }
+
+ if (address['type'] === 'fan') {
+ newRow.classList.add('fan');
+ newRow
+ .querySelector('.info-type')
+ .querySelectorAll('span')[1].textContent = 'Fan';
+ } else {
+ newRow
+ .querySelector('.info-type')
+ .querySelectorAll('span')[1].textContent = 'Standard';
+ }
+
+ if (address['is_gate_online'] === '0') {
+ newRow.classList.add('offline');
+ newRow.querySelector('.status').querySelectorAll('span')[1].textContent =
+ 'Offline';
+ } else {
+ newRow.querySelector('.status').querySelectorAll('span')[1].textContent =
+ 'Online';
+ }
+
+ if (address['ip_address']) {
+ newRow
+ .querySelector('.info-coord')
+ .querySelectorAll('span')[1].textContent = updateIp(
+ address['ip_address'],
+ );
+ } else {
+ newRow
+ .querySelector('.info-coord')
+ .querySelectorAll('span')[0].textContent = '';
+ newRow
+ .querySelector('.info-coord')
+ .querySelectorAll('span')[1].textContent = '';
+ }
+
+ newRow
+ .querySelector('.info-const')
+ .querySelectorAll('span')[1].textContent = Math.abs(
+ hash(JSON.stringify(address)),
+ );
+
+ newRow
+ .querySelector('.info-ref')
+ .querySelectorAll('span')[1].textContent = `${Math.floor(
+ Math.random() * 999,
+ )
+ .toString()
+ .padStart(3, '0')}x${Math.floor(Math.random() * 9999)}`;
+
+ newRow.querySelector('.info-a').innerHTML = generatePlanetData();
+
+ tableBody.appendChild(aTag);
+ });
+
+ sortData('name');
+}
+
+function sortData(sortProperty) {
+ addresses.sort((a, b) => a.name.localeCompare(b.name));
+
+ if (sortProperty === 'type') {
+ addresses.sort((a, b) => a.type.localeCompare(b.type));
+ } else if (sortProperty === 'status') {
+ addresses.sort(
+ (a, b) =>
+ a.type.localeCompare(b.type) ||
+ (b.is_gate_online ?? '0').localeCompare(a.is_gate_online ?? '0'),
+ );
+ } else if (sortProperty === 'glyph') {
+ addresses.sort((a, b) =>
+ a.gate_address
+ .map(x => x.toString().padStart(3, '0'))
+ .join('')
+ .localeCompare(
+ b.gate_address.map(x => x.toString().padStart(3, '0')).join(''),
+ ),
+ );
+ }
+
+ addresses.forEach(address => {
+ tableBody.appendChild(address.htmlData);
+ });
+
+ tableBody.scrollTo(0, 0);
+}
+
+const labelOptions = {
+ Mission: [
+ 'Complete',
+ 'Pending',
+ 'Active',
+ 'Failed',
+ 'Aborted',
+ 'Ongoing',
+ 'Incomplete',
+ 'Success',
+ 'Recon',
+ 'Contact',
+ 'Delayed',
+ ],
+ Survey: [
+ 'Complete',
+ 'Partial',
+ 'In progress',
+ 'Failed',
+ 'Limited scan',
+ 'Area mapped',
+ 'Outpost found',
+ 'No data',
+ 'Hazard zones',
+ 'Energy spike',
+ 'Inconclusive',
+ ],
+ Terrain: [
+ 'Mixed',
+ 'Plateau',
+ 'Valley',
+ 'Forest',
+ 'Desert',
+ 'Jungle',
+ 'Glacier',
+ 'Marsh',
+ 'Tundra',
+ 'Oasis',
+ 'Ocean',
+ ],
+ Life: ['Detected', 'Unknown', 'Extinct'],
+ Team: [
+ 'Deployed',
+ 'Returned',
+ 'Missing',
+ 'Awaiting evac',
+ 'En route',
+ 'No contact',
+ 'In orbit',
+ 'Standing by',
+ 'Request backup',
+ 'Safe',
+ ],
+ Tech: [
+ 'Unknown',
+ 'Primitive',
+ 'Advanced',
+ 'Inactive',
+ 'Damaged',
+ 'Non-native',
+ 'Functional',
+ 'No signs',
+ ],
+ Scan: [
+ 'Normal',
+ 'Stable',
+ 'Interference',
+ 'Disturbance',
+ 'Power reading',
+ 'Active signals',
+ 'Structural mass',
+ 'Unknown field',
+ 'Signal trace',
+ 'Bio signature',
+ ],
+ Air: [
+ 'Breathable',
+ 'Thin',
+ 'Toxic',
+ 'Heavy gases',
+ 'Oxygen rich',
+ 'Unstable',
+ 'Safe level',
+ 'Mask required',
+ 'Contaminated',
+ 'No trace',
+ ],
+ 'O2 Level': [
+ '19%',
+ '20%',
+ '21%',
+ '22%',
+ '23%',
+ '18%',
+ '17% (Low)',
+ '24%',
+ '16% (Risk)',
+ '25%',
+ ],
+ Temp: [
+ '5°C',
+ '12°C',
+ '18°C',
+ '22°C',
+ '27°C',
+ '30°C',
+ '-5°C (Cold)',
+ '35°C (Hot)',
+ '0°C',
+ '15°C',
+ ],
+ Gravity: [
+ '0.8G',
+ '0.9G',
+ '1.0G',
+ '1.1G',
+ '1.2G',
+ '0.95G',
+ '1.05G',
+ '0.7G (Low)',
+ '1.3G (High)',
+ '1.15G',
+ ],
+ Rad: [
+ '0.1 mSv/h',
+ '0.2 mSv/h',
+ '0.05 mSv/h',
+ '0.3 mSv/h',
+ '0.4 mSv/h',
+ '0.0 mSv/h',
+ '0.6 mSv/h',
+ '0.9 mSv/h (Caution)',
+ '0.8 mSv/h',
+ '0.25 mSv/h',
+ ],
+ Atm: [
+ '0.9 bar',
+ '1.0 bar',
+ '1.1 bar',
+ '1.2 bar',
+ '0.95 bar',
+ '1.05 bar',
+ '0.8 bar (Thin)',
+ '1.3 bar (Dense)',
+ '0.85 bar',
+ '1.15 bar',
+ ],
+ Seismic: [
+ 'None',
+ 'Low',
+ 'Minor tremors',
+ 'Stable',
+ '0.3 Hz',
+ '0.5 Hz',
+ 'Microquakes',
+ '0.1 Hz',
+ 'Irregular',
+ '0.0 Hz',
+ ],
+ 'Mag Field': [
+ '45 μT',
+ '50 μT',
+ '38 μT',
+ '60 μT',
+ '42 μT',
+ '55 μT',
+ '48 μT',
+ '35 μT',
+ '65 μT',
+ '40 μT',
+ ],
+ Wind: [
+ '0 km/h',
+ '5 km/h',
+ '12 km/h',
+ '8 km/h',
+ '20 km/h',
+ '15 km/h',
+ '25 km/h (Gusty)',
+ '3 km/h',
+ '10 km/h',
+ '18 km/h',
+ ],
+};
+const notes = [
+ 'No anomalies',
+ 'Culture noted',
+ 'Further Review',
+ 'Awaiting orders',
+ 'Culture observed',
+ 'Gate unstable',
+ 'Threat detected',
+ 'Artifacts logged',
+ 'Scan incomplete',
+ 'Base camp set',
+ 'Unknown energy',
+ 'Transmission lost',
+ 'Possible ruins',
+ 'Minimal contact',
+ 'Team under review',
+ 'Quarantine zone',
+ 'Language unknown',
+ 'Debris analyzed',
+ 'Unreadable data',
+ 'Life signs low',
+ 'Survey incomplete',
+ 'Hostile encounter',
+ 'Pre-warp civ',
+ 'Tech interference',
+ 'Records missing',
+ 'Sensors limited',
+ 'Limited access',
+ 'Return scheduled',
+];
+
+// Generates 3 random attributes from the dict of labelOptions
+// Small chance of adding a Notes section
+// Even smaller chance of redacting the value
+function generatePlanetData() {
+ let toReturn = [];
+ let options = Object.keys(labelOptions);
+ for (let i = 0; i < 3; i++) {
+ const index = Math.floor(Math.random() * options.length);
+ const entry = options.splice(index, 1);
+ const optionIndex = Math.floor(Math.random() * labelOptions[entry].length);
+ const redacted = Math.random() < 0.02;
+ if (redacted) {
+ toReturn.push(`${entry}: *****`);
+ } else {
+ toReturn.push(`${entry}: ${labelOptions[entry][optionIndex]}`);
+ }
+ }
+ if (Math.random() < 0.15) {
+ // Add Note
+ const noteIndex = Math.floor(Math.random() * notes.length);
+ toReturn.push(`Note: ${notes[noteIndex]}`);
+ }
+
+ return toReturn.join('
');
+}
diff --git a/web/retro/js/crt.js b/web/retro/js/crt.js
new file mode 100644
index 0000000..bd806bf
--- /dev/null
+++ b/web/retro/js/crt.js
@@ -0,0 +1,35 @@
+/* USER CUSTOMIZATIONS */
+
+const CRT_SCREEN_FLICKER = false;
+
+/* DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU'RE DOING!!!! */
+
+const elem = document.body;
+
+function toggleFlicker() {
+ const shouldFlicker = Math.random() > 0.7; // 30% chance to flicker
+ if (shouldFlicker) {
+ elem.classList.add("flicker");
+ setTimeout(() => {
+ elem.classList.remove("flicker");
+ toggleFlicker();
+ }, 1000); // flicker for 200ms
+ } else {
+ setTimeout(toggleFlicker, 300); // check every 300ms
+ }
+}
+
+// Run it every random interval
+if(CRT_SCREEN_FLICKER) {
+ setTimeout(toggleFlicker, 300);
+}
+
+const distortion = document.querySelector('.crt-distortion');
+let $rand = 0;
+distortion.addEventListener('animationend', function(){
+ this.classList.remove("scanline-animation");
+ $rand = (Math.floor(Math.random() * 6) + 4);
+ this.style.animationDuration = $rand + 's';
+ void this.offsetWidth; // hack to reflow css animation
+ this.classList.add("scanline-animation");
+});
diff --git a/web/retro/js/dial.js b/web/retro/js/dial.js
new file mode 100644
index 0000000..82535f1
--- /dev/null
+++ b/web/retro/js/dial.js
@@ -0,0 +1,616 @@
+/* USER CUSTOMIZATIONS */
+
+// Spinning is so much cooler than not spinning
+const RING_ANIMATION = true;
+
+const AUTHORIZATION_CODE_RANDOMIZE = true;
+const AUTHORIZATION_CODE = '77892757892387';
+
+const USER = 'SGT. W HARRIMAN';
+
+// Used when gate is offline initially
+const DEFAULT_GATE_NAME = 'STARGATE';
+
+const TEXT_OFFLINE = 'OFFLINE';
+const TEXT_IDLE = 'IDLE';
+const TEXT_DIALING = 'DIALING';
+const TEXT_INCOMING = 'INCOMING';
+const TEXT_ENGAGED = 'ENGAGED';
+
+/* DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU'RE DOING!!!! */
+
+const glyph = document.querySelector('.glyph');
+const appendTarget = document.querySelector('.dial-append');
+const timer = document.querySelector('.timer');
+const gateName = document.querySelector('.gate-name');
+const destination = document.querySelector('.destination');
+const destinationGlyphs = document.querySelector('.destination-glyphs');
+const ring1 = document.querySelector('.ring-1 circle');
+const ring3 = document.querySelector('.ring-3');
+const infoText = document.querySelector('.info-box');
+const border = document.querySelector('.border');
+const keyboard = document.querySelector('.keyboard');
+const systemEl = document.querySelector('.system');
+const authCode = document.querySelector('.auth-code');
+const statusEl = document.querySelector('.status');
+
+let statusInterval;
+
+const STATE_ACTIVE = 'active';
+const STATE_IDLE = 'idle';
+const STATE_DIAL_OUT = 'dialing_out';
+const STATE_DIAL_IN = 'dialing_in';
+
+let speedDialAddress = [];
+
+let encoding = false;
+let state = STATE_IDLE;
+
+let gateStatus = {};
+let firstStatus = true;
+let fetchingStatus = false;
+
+let buffer = [];
+let bufferIndex = 0;
+
+// For animating glyph ring
+let lastRingPos = -1;
+let gateMoving = false;
+let lastGateRotation = 0;
+
+let lockedGlyphs = {};
+let locked_chevrons = 0;
+
+let symbols = [];
+
+
+// INITIALIZE --------------------------------------------------------------------------
+async function initialize_computer() {
+ initialize_text();
+
+ const responseSymbols = await fetch('/stargate/get/symbols_all');
+ if (!responseSymbols.ok) {
+ handleOffline();
+ return;
+ }
+ symbols = await responseSymbols.json();
+
+ buildKeyboard();
+ updateStatusFrequency(5000);
+ speedDialStart();
+}
+initialize_computer();
+
+function updateStatusFrequency(ms) {
+ clearInterval(statusInterval);
+ statusInterval = setInterval(watch_dialing_status, ms);
+ watch_dialing_status();
+}
+
+// KEYBOARD DIALING ----------------------------------------------------------------------
+document.body.onkeydown = function (e) {
+ const charCode = typeof e.which == 'number' ? e.which : e.keyCode;
+ if (charCode > 0) {
+ const code = String.fromCharCode(charCode);
+ const symbol = symbols.find(x => x.keyboard_mapping === code);
+ if (symbol) {
+ dhd_press(`${symbol.index}`);
+ } else if (code === ' ' || code === '\r') {
+ dhd_press('0');
+ }
+ }
+};
+
+// SPEED DIALING --------------------------------------------------------------------------
+async function speedDialStart() {
+ const parts = window.location.search.substring(1).split('&');
+ const query = {};
+ parts.forEach(part => {
+ const temp = part.split('=');
+ query[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
+ });
+
+ if (query.address) {
+ speedDialAddress = [...query.address.split('-').map(Number), 1, 0];
+ window.history.pushState({}, document.title, window.location.pathname);
+ await clear_buffer();
+ speedDial();
+ }
+}
+
+async function speedDial() {
+ if (speedDialAddress.length > 0) {
+ const a = speedDialAddress.splice(0, 1);
+ dhd_press(`${a}`);
+ setTimeout(speedDial, 1500);
+ }
+}
+
+// STATUS UPDATES --------------------------------------------------------------------------
+async function watch_dialing_status() {
+ if (fetchingStatus) {
+ // Skip update until previous request finishes.
+ return;
+ }
+
+ try {
+ fetchingStatus = true;
+ const responseStatus = await fetch('/stargate/get/dialing_status');
+ if (!responseStatus.ok) {
+ handleOffline();
+ fetchingStatus = false;
+ return;
+ }
+ gateStatus = await responseStatus.json();
+
+ const initialState = state;
+ updateText(gateName, gateStatus.gate_name);
+
+ let [new_locked_chevrons, hardBreak] = handleActiveGate();
+
+ if (!hardBreak) {
+ new_locked_chevrons = handleDialingOut(new_locked_chevrons);
+ new_locked_chevrons = handleDialingIn(new_locked_chevrons);
+
+ while (locked_chevrons < new_locked_chevrons) {
+ lock(locked_chevrons);
+ locked_chevrons += 1;
+ if (!encoding) {
+ dial();
+ }
+ }
+
+ if (!encoding) {
+ this.firstStatus = false;
+ }
+
+ trySpinning();
+ }
+
+ updateState();
+ updateTimer(gateStatus.wormhole_time_till_close);
+ updateText(destination, gateStatus.connected_planet);
+
+ if (initialState !== state) {
+ if (state === STATE_IDLE) {
+ updateStatusFrequency(5000);
+ } else {
+ updateStatusFrequency(500);
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ handleOffline();
+ }
+ fetchingStatus = false;
+}
+
+function handleActiveGate(new_locked_chevrons = 0) {
+ if (
+ state === STATE_ACTIVE &&
+ !gateStatus.wormhole_active &&
+ gateStatus.address_buffer_incoming.length === 0 &&
+ gateStatus.address_buffer_outgoing.length === 0
+ ) {
+ resetGate();
+ updateStatusFrequency(5000);
+ return [0, true];
+ } else if (state !== STATE_ACTIVE && gateStatus.wormhole_active) {
+ // Active Incoming
+ if (gateStatus.address_buffer_incoming.length > 0) {
+ buffer = gateStatus.address_buffer_incoming;
+ new_locked_chevrons = gateStatus.locked_chevrons_incoming;
+ if (state === STATE_DIAL_OUT) {
+ resetGate();
+ }
+ }
+ // Active Outgoing
+ if (gateStatus.address_buffer_outgoing.length > 0) {
+ buffer = gateStatus.address_buffer_outgoing;
+ new_locked_chevrons = gateStatus.locked_chevrons_outgoing;
+ const toRemove = document.querySelectorAll('.destination-glyphs img');
+ toRemove.forEach(g => g.remove());
+ if (state === STATE_DIAL_IN) {
+ resetGate();
+ }
+ }
+
+ state = STATE_ACTIVE;
+ dial();
+ }
+ return [new_locked_chevrons, false];
+}
+
+function handleDialingOut(new_locked_chevrons) {
+ if (
+ state === STATE_DIAL_OUT &&
+ gateStatus.address_buffer_outgoing.length === 0
+ ) {
+ resetGate();
+ } else if (
+ state !== STATE_DIAL_OUT &&
+ state !== STATE_ACTIVE &&
+ gateStatus.address_buffer_outgoing.length > 0
+ ) {
+ resetGate();
+ state = STATE_DIAL_OUT;
+ }
+
+ // Dialing Out
+ if (state === STATE_DIAL_OUT) {
+ let bufferChange =
+ gateStatus.address_buffer_outgoing.length - buffer.length;
+ buffer = gateStatus.address_buffer_outgoing;
+ if (bufferChange > 0) {
+ updateDestination(bufferChange);
+ setKeysDisabled(buffer);
+ if (!encoding) {
+ dial();
+ }
+ }
+ new_locked_chevrons = gateStatus.locked_chevrons_outgoing;
+ }
+ return new_locked_chevrons;
+}
+
+function handleDialingIn(new_locked_chevrons) {
+ if (
+ state === STATE_DIAL_IN &&
+ gateStatus.address_buffer_incoming.length === 0
+ ) {
+ resetGate();
+ } else if (
+ state !== STATE_DIAL_IN &&
+ state !== STATE_ACTIVE &&
+ gateStatus.address_buffer_incoming.length > 0
+ ) {
+ resetGate();
+ state = STATE_DIAL_IN;
+ }
+
+ if (state === STATE_DIAL_IN) {
+ let bufferChange =
+ gateStatus.address_buffer_incoming.length - buffer.length;
+ buffer = gateStatus.address_buffer_incoming;
+ if (bufferChange > 0) {
+ if (!encoding) {
+ dial();
+ }
+ }
+ new_locked_chevrons = gateStatus.locked_chevrons_incoming;
+ }
+
+ return new_locked_chevrons;
+}
+
+// That's a neat trick
+function trySpinning() {
+ if (!RING_ANIMATION) {
+ return;
+ }
+
+ if (lastRingPos === -1) {
+ lastRingPos = gateStatus.ring_position;
+ } else if (lastRingPos !== gateStatus.ring_position) {
+ lastRingPos = gateStatus.ring_position;
+ ring3.classList.add('rotating');
+ ring3.classList.remove('slow-rotate');
+ gateMoving = true;
+ } else if (gateMoving) {
+ gateMoving = false;
+ stopSpinning(ring3);
+ }
+}
+
+function stopSpinning(el) {
+ // Step 1: Capture current computed transform (rotation)
+ const computedStyle = window.getComputedStyle(el);
+ const matrix = new DOMMatrixReadOnly(computedStyle.transform);
+
+ // Calculate current rotation angle in degrees
+ let angle = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
+ if (angle < 0) angle += 360;
+ angle = angle % 360;
+
+ // Step 2: Remove animation
+ el.classList.remove('rotating');
+
+ // Step 3: Apply current rotation as a static transform
+ el.style.transform = `rotate(${angle}deg)`;
+
+ // Force reflow to flush style changes
+ void el.offsetWidth;
+
+ // Step 4: Add transition and apply a final slow rotation
+ el.classList.add('slow-rotate');
+
+ // Rotate 10° more over 1s (simulate deceleration)
+ el.style.transform = `rotate(${angle + 10}deg)`;
+
+ // Optional cleanup after transition
+ el.addEventListener(
+ 'transitionend',
+ () => {
+ el.classList.remove('slow-rotate');
+ lastGateRotation = (angle + 10 + lastGateRotation) % 360;
+ el.style.rotate = `${lastGateRotation}deg`;
+ el.style.transform = '';
+ },
+ {once: true},
+ );
+}
+
+async function dhd_press(symbol, key) {
+ if (state === STATE_DIAL_OUT && buffer.find(x => `${x}` === symbol)) {
+ return;
+ }
+
+ key?.classList.add('disabled');
+ await fetch('/stargate/do/dhd_press', {
+ method: 'POST',
+ body: JSON.stringify({symbol}),
+ mode: 'no-cors',
+ });
+ setTimeout(() => watch_dialing_status(), 300);
+}
+
+async function clear_buffer() {
+ await fetch('/stargate/do/clear_outgoing_buffer', {
+ method: 'POST',
+ mode: 'no-cors',
+ });
+ setTimeout(() => watch_dialing_status(), 300);
+}
+
+function dial() {
+ if (buffer.length === 0) {
+ encoding = false;
+ bufferIndex = 0;
+ firstStatus = false;
+ return;
+ }
+
+ if (bufferIndex >= locked_chevrons) {
+ if (buffer.length > bufferIndex) {
+ displayGlyph();
+ }
+ encoding = false;
+ return;
+ }
+
+ encoding = true;
+
+ if (bufferIndex < 9) {
+ const [newGlyph, newGlyph2] = displayGlyph();
+ if (firstStatus) {
+ newGlyph.classList.add('locked');
+ newGlyph2.classList.add('locked');
+ } else {
+ setTimeout(() => newGlyph.classList.add('locked'), 1);
+ setTimeout(() => newGlyph2.classList.add('locked'), 50);
+ }
+ }
+ bufferIndex += 1;
+
+ if (gateStatus.wormhole_active || firstStatus) {
+ // Allow quick locking for active wormholes or partially dialed gates on page load
+ setTimeout(dial, 300);
+ } else {
+ // Add some delay between chevrons so previous animations can finish
+ setTimeout(dial, 1300);
+ }
+}
+
+function displayGlyph() {
+ if (lockedGlyphs[bufferIndex] !== undefined) {
+ return lockedGlyphs[bufferIndex];
+ }
+ const glyphIndex = buffer[bufferIndex];
+ const symbol = symbols.find(x => x['index'] === glyphIndex);
+
+ const newGlyph = glyph.cloneNode(true);
+ newGlyph.classList.remove('hidden');
+ newGlyph.src = '';
+ newGlyph.src = '..' + symbol['imageSrc'];
+
+ newGlyph.classList.add(`g${bufferIndex + 1}`);
+ const newGlyph2 = newGlyph.cloneNode(true);
+ newGlyph2.classList.add('blur');
+ appendTarget.append(newGlyph2);
+ appendTarget.append(newGlyph);
+
+ lockedGlyphs[bufferIndex] = [newGlyph, newGlyph2];
+ return [newGlyph, newGlyph2];
+}
+
+function lock(i) {
+ // Only allow locking 7 chevrons
+ if (i >= 9) {
+ return;
+ }
+
+ startDrawingPath(i + 1);
+
+ const chevrons = document.querySelectorAll(
+ `.chevron-${i + 1},.chevron-state-${i + 1}`,
+ );
+ chevrons.forEach(c => c.classList.add('locked'));
+
+ const b = document.querySelector(`.b${i + 1}`);
+ if (b) {
+ const newB = b.cloneNode(true);
+ newB.classList.add(`clip-${i < 3 ? '2' : '1'}`);
+ appendTarget.append(newB);
+ setTimeout(() => newB.classList.add('locked'), 10);
+ }
+}
+
+// Clear all animations and get gate back into initial state
+function resetGate() {
+ const chevrons = document.querySelectorAll(
+ '.gate.chevron.locked,.chevron-states tr.locked',
+ );
+ chevrons.forEach(c => c.classList.remove('locked'));
+
+ const toRemove = document.querySelectorAll(
+ '.dial-append > *,.destination-glyphs img',
+ );
+ toRemove.forEach(g => g.remove());
+
+ const keys = document.querySelectorAll('.keyboard div');
+ keys.forEach(k => k.classList.remove('disabled'));
+
+ state = STATE_IDLE;
+ encoding = false;
+ buffer = [];
+ bufferIndex = 0;
+ lockedGlyphs = {};
+ locked_chevrons = 0;
+}
+
+function updateState() {
+ if (state === STATE_ACTIVE) {
+ setTimeout(() => {
+ updateText(infoText, 'ENGAGED');
+ border.classList.add('active');
+ border.classList.remove('idle');
+
+ if (gateStatus.black_hole_connected) {
+ ring1.setAttribute('fill', 'url(#radialGradientDanger)');
+ } else {
+ ring1.setAttribute('fill', 'url(#radialGradient)');
+ }
+ }, 500);
+ } else if (state === STATE_DIAL_OUT) {
+ updateText(infoText, 'DIALING');
+ border.classList.remove('active');
+ border.classList.remove('idle');
+ } else if (state === STATE_DIAL_IN) {
+ updateText(infoText, 'INCOMING');
+ border.classList.remove('active');
+ border.classList.remove('idle');
+ } else {
+ updateText(infoText, 'IDLE');
+ border.classList.remove('active');
+ border.classList.add('idle');
+ }
+}
+
+function updateDestination(lastXGlyphs) {
+ const toAdd = buffer.slice(buffer.length - lastXGlyphs);
+ toAdd.forEach(g => {
+ const symbol = symbols.find(x => x['index'] === g);
+ const glyph = document.createElement('img');
+ glyph.src = '..' + symbol['imageSrc'];
+ destinationGlyphs.append(glyph);
+ });
+}
+
+function updateTimer(secondsLeft) {
+ if (gateStatus.black_hole_connected) {
+ secondsLeft =
+ 38 * 60 -
+ (gateStatus.wormhole_max_time - gateStatus.wormhole_time_till_close);
+ }
+
+ const mins = Math.max(0, Math.floor(secondsLeft / 60));
+ const secs = Math.max(0, secondsLeft % 60);
+ updateText(timer, `${mins}:${secs.toString().padStart(2, '0')}`);
+}
+
+function initialize_text() {
+ updateText(gateName, DEFAULT_GATE_NAME);
+ updateText(systemEl.children.item(0), 'USER: ' + USER);
+
+ const codeLength = Math.max(AUTHORIZATION_CODE.length, 15);
+ for (let i = 0; i < codeLength; i++) {
+ if (i !== 6) {
+ let code = AUTHORIZATION_CODE[i];
+
+ if (AUTHORIZATION_CODE_RANDOMIZE) {
+ code = Math.floor(Math.random() * 10);
+ }
+
+ updateText(authCode.children.item(i), code);
+ }
+ }
+}
+
+function handleOffline() {
+ updateText(infoText, TEXT_OFFLINE);
+ // setKeysDisabled([...symbols.map(x => x.index), 0, -1]);
+}
+
+function buildKeyboard() {
+ symbols.forEach(symbol => {
+ if (symbol.keyboard_mapping) {
+ const keyWrapper = document.createElement('div');
+ keyWrapper.classList.add(`symbol-${symbol.index}`);
+ const img = document.createElement('img');
+ img.src = '..' + symbol.imageSrc;
+ img.onclick = () => dhd_press(`${symbol.index}`, img);
+ keyWrapper.appendChild(img);
+ keyboard.appendChild(keyWrapper);
+ }
+ });
+
+ const keyWrapper = document.createElement('div');
+ keyWrapper.classList.add(`symbol-0`);
+ const img = document.createElement('img');
+ img.src = `images/dhd.svg`;
+ img.onclick = () => dhd_press(`0`, img);
+ keyWrapper.appendChild(img);
+ keyboard.appendChild(keyWrapper);
+}
+
+// Locking pre-dialed keys from the keyboard
+function setKeysDisabled(keys, disabled = true) {
+ keys.forEach(k => setKeyDisabled(k, disabled));
+}
+function setKeyDisabled(glyphIndex, disabled) {
+ const key = document.querySelector(`.keyboard .symbol-${glyphIndex}`);
+ if (key) {
+ if (disabled) {
+ key.classList.add('disabled');
+ } else {
+ key.classList.remove('disabled');
+ }
+ }
+}
+
+// Animate the power line from the chevron to the glyph box
+const pathTime = 800; // ms
+function startDrawingPath(index) {
+ const svgBase = document.querySelector(`.cl${index}`);
+ if (svgBase) {
+ const svg = svgBase.cloneNode(true);
+ const path = svg.querySelector('path');
+ const pathLength = path.getTotalLength();
+
+ svg.classList.add('locked');
+ path.style.stroke = 'var(--color-danger)';
+ path.style.strokeWidth = '6';
+ appendTarget.append(svg);
+
+ const start = performance.now();
+ let length = 0;
+ function animate(now) {
+ const time = now - start;
+ length = Math.floor((time / pathTime) * pathLength);
+ path.style.strokeDasharray = [length, pathLength].join(' ');
+
+ if (length < pathLength) {
+ requestAnimationFrame(animate);
+ }
+ }
+ animate(start);
+ } else {
+ // No chevron link path, probably 8th/9th chevron
+ }
+}
+
+function updateText(elem, text) {
+ if (elem.textContent !== text) {
+ elem.textContent = text ?? '';
+ }
+}
diff --git a/web/retro/js/navigation.js b/web/retro/js/navigation.js
new file mode 100644
index 0000000..ce455bd
--- /dev/null
+++ b/web/retro/js/navigation.js
@@ -0,0 +1,88 @@
+/* When the user clicks on the button,
+toggle between hiding and showing the dropdown content */
+function openDropdown(elementId) {
+ document.getElementById(elementId).classList.toggle('show');
+}
+
+// Close the dropdown menu if the user clicks outside of it
+window.onclick = function (event) {
+ if (!event.target.matches('.dropbtn')) {
+ const dropdowns = document.getElementsByClassName('dropdown-content');
+ for (let i = 0; i < dropdowns.length; i++) {
+ const openDropdown = dropdowns[i];
+ if (openDropdown.classList.contains('show')) {
+ openDropdown.classList.remove('show');
+ }
+ }
+ }
+};
+
+async function restart() {
+ const response = confirm(
+ 'Are you sure you want to restart the Gate software?',
+ );
+ if (response) {
+ await fetch('/stargate/do/restart', {
+ method: 'POST',
+ mode: 'no-cors',
+ });
+ }
+}
+
+async function reboot() {
+ const response = confirm(
+ 'Are you sure you want to restart the Raspberry Pi?',
+ );
+ if (response) {
+ await fetch('/stargate/do/reboot', {
+ method: 'POST',
+ mode: 'no-cors',
+ });
+ }
+}
+
+async function shutdown() {
+ const response = confirm(
+ 'Are you sure you want to shutdown the Raspberry Pi?',
+ );
+ if (response) {
+ await fetch('/stargate/do/shutdown', {
+ method: 'POST',
+ mode: 'no-cors',
+ });
+ }
+}
+
+function isActive(href) {
+ console.log(window.location.href);
+ return `href="${href}"` + (window.location.href.includes(href) ? 'class="active-link"' : '');
+}
+
+function initializeNavBar() {
+ const div = document.createElement('div');
+ div.innerHTML = `
+
+ `;
+ const innerDiv = div.querySelector('div');
+ const body = document.querySelector('body')
+ body.insertBefore(innerDiv, body.childNodes[0])
+}
+initializeNavBar();
diff --git a/web/symbol_overview.htm b/web/symbol_overview.htm
index 7d69e10..9310a1e 100644
--- a/web/symbol_overview.htm
+++ b/web/symbol_overview.htm
@@ -50,10 +50,10 @@