Changes
diff --git a/button-maker/index.html b/button-maker/index.html
new file mode 100644
index 0000000..6e5aba0
--- /dev/null
+++ b/button-maker/index.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang="en-US">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport"
+ content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" type="text/css" href="styles.css" media="screen" />
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap" rel="stylesheet">
+
+ <link rel="stylesheet" href="styles.css" />
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/preact/10.27.1/preact.min.js"
+ crossorigin="anonymous"
+ referrerpolicy="no-referrer"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/preact/10.27.1/hooks.umd.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+ <script src="scripts/lib.js"></script>
+ <script type="module" defer src="scripts/main.js"></script>
+ <title>Button maker</title>
+ </head>
+ <body>
+ <div class="Window" id="MainWindow">
+ <h1 class="Titlebar">Button Maker</h1>
+ <div id="App">
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/button-maker/scripts/lib.js b/button-maker/scripts/lib.js
new file mode 100644
index 0000000..372d9d7
--- /dev/null
+++ b/button-maker/scripts/lib.js
@@ -0,0 +1,79 @@
+// In Firefox (on Mac only?), for <input type="color">, setting value
+// to abbreviated hex form (e.g. #123) doesn’t work. So all colors
+// should be specified in full form.
+const DEFAULT_NETSCAPE_PARAMS = {
+ color_bg: "#c0c0c0",
+ logo_url: "https://upload.wikimedia.org/wikipedia/commons/0/08/Netscape_icon.svg",
+ text_top: {
+ content: "Netscape",
+ pos: [31, 11],
+ font_size: 10.5,
+ color: "#000000",
+ },
+ text_bottom: {
+ content: "Now!",
+ pos: [30, 28],
+ font_size: 17.5,
+ color: "#ff0000",
+ },
+ banner: {
+ style: "corner_bottom_right", // corner_bottom_right or right
+ color: "#008080",
+ text: {
+ content: "5.0",
+ pos: [65, 46],
+ font_size: 8,
+ color: "#ffffff",
+ },
+ },
+};
+
+const DEFAULT_APP_STATE = {
+ button_type: "netscape",
+ button_params: DEFAULT_NETSCAPE_PARAMS,
+}
+
+const SVG_ATTRIBUTES = {viewBox: "0 0 88 31", width: 88, height: 31};
+
+// Return three integers as an array.
+function parseHexColor(color_str)
+{
+ console.debug("Parsing", color_str);
+ m = color_str.match(/^#([0-9a-f]{3})$/i);
+ if(m)
+ {
+ // in three-character format, each value is multiplied by 0x11 to give an
+ // even scale from 0x00 to 0xff
+ return [
+ parseInt(m[1].charAt(0),16)*0x11,
+ parseInt(m[1].charAt(1),16)*0x11,
+ parseInt(m[1].charAt(2),16)*0x11
+ ];
+ }
+
+ m = color_str.match(/^#([0-9a-f]{6})$/i);
+ if(m)
+ {
+ return [
+ parseInt(m[1].substr(0,2),16),
+ parseInt(m[1].substr(2,2),16),
+ parseInt(m[1].substr(4,2),16)
+ ];
+ }
+ return null;
+}
+
+function lighten(rgb, delta)
+{
+ return [rgb[0] + delta, rgb[1] + delta, rgb[2] + delta];
+}
+
+function darken(rgb, delta)
+{
+ return [rgb[0] - delta, rgb[1] - delta, rgb[2] - delta];
+}
+
+function color2Str(color)
+{
+ return `rgb(${color[0]} ${color[1]} ${color[2]})`;
+}
diff --git a/button-maker/scripts/main.js b/button-maker/scripts/main.js
new file mode 100644
index 0000000..7232064
--- /dev/null
+++ b/button-maker/scripts/main.js
@@ -0,0 +1,279 @@
+const h = preact.h;
+
+function ButtonViewNetscape({app_state})
+{
+ const params = app_state.button_params;
+ console.debug("Rendering netscape params", params);
+ let color_bg = parseHexColor(params.color_bg);
+ let color_hilight_1 = color2Str(lighten(color_bg, 63));
+ let color_hilight_2 = color2Str(lighten(color_bg, 31));
+ let color_shadow_1 = color2Str(darken(color_bg, 0xc0 - 0xa));
+ let color_shadow_2 = color2Str(darken(color_bg, 0xc0 - 0x54));
+
+ let ribbon = null;
+ let ribbon_text = null;
+ if(app_state.button_params.banner.style == "corner_bottom_right")
+ {
+ ribbon = h("polygon", {points: "86,8 86,24 81,29 65,29",
+ fill: params.banner.color});
+ ribbon_text = h("text", {
+ x: params.banner.text.pos[0], y: params.banner.text.pos[1],
+ "font-family": "sans-serif",
+ "dominant-baseline": "middle", "text-anchor": "middle",
+ "transform-origin": "center", "transform":"rotate(-45)",
+ "font-weight": "bold", "font-size": params.banner.text.font_size,
+ fill: params.banner.text.color, "stroke-width": 0},
+ params.banner.text.content);
+ }
+ else if(app_state.button_params.banner.style == "right")
+ {
+ ribbon = h("rect", {x: 78, y: 2, width: 8, height: 27,
+ fill: app_state.button_params.banner.color});
+ ribbon_text = h("text", {
+ x: params.banner.text.pos[0], y: params.banner.text.pos[1],
+ "font-family": "sans-serif", "dominant-baseline": "middle",
+ "text-anchor": "middle", "transform-origin": "center",
+ transform: "rotate(-90)", "font-weight": "bold",
+ "font-size": params.banner.text.font_size,
+ fill: params.banner.text.color, "stroke-width": 0},
+ params.banner.text.content);
+ }
+
+ return h("svg", SVG_ATTRIBUTES,
+ h("g", {"stroke-width": 0},
+ h("polygon", {points: "0,0 88,0 87,1 1,1",
+ fill: color_hilight_1}),
+ h("polygon", {points: "1,1 87,1 86,2 2,2",
+ fill: color_hilight_2}),
+ h("polygon", {points: "88,0 88,31 87,30 87,1",
+ fill: color_shadow_1}),
+ h("polygon", {points: "87,1 87,30 86,29 86,2",
+ fill: color_shadow_2}),
+ h("polygon", {points: "88,31 0,31 1,30 87,30",
+ fill: color_shadow_1}),
+ h("polygon", {points: "87,30 1,30 2,29,86,29",
+ fill: color_shadow_2}),
+ h("polygon", {points: "0,0 1,1 1,30 0,31",
+ fill: color_hilight_1}),
+ h("polygon", {points: "1,1 2,2 2,29 1,30",
+ fill: color_hilight_2}),
+ h("rect", {x: 2, y: 2, width: 84, height: 27,
+ fill: params.color_bg}),
+ ribbon),
+ h("image", {href: params.logo_url, x: 3, y: 3, width: 25,
+ height: 25}),
+ h("text", {x: params.text_bottom.pos[0],
+ y: params.text_bottom.pos[1], "font-family": "Kalam",
+ "font-size": params.text_bottom.font_size,
+ fill: params.text_bottom.color, "stroke-width": 0},
+ "Now!"),
+ h("text", {x: params.text_top.pos[0], y: params.text_top.pos[1],
+ "font-family": "sans-serif",
+ "font-size": params.text_top.font_size,
+ fill: params.text_top.color, "stroke-width": 0},
+ params.text_top.content),
+ ribbon_text);
+}
+
+function TextControlView({params, on_change})
+{
+ console.debug("Rendering text params", params);
+ function onContentChange(e)
+ {
+ let new_params = structuredClone(params);
+ new_params.content = e.target.value;
+ on_change(new_params);
+ }
+
+ function onPosXChange(e)
+ {
+ let new_params = structuredClone(params);
+ new_params.pos[0] = parseFloat(e.target.value);
+ on_change(new_params);
+ }
+
+ function onPosYChange(e)
+ {
+ let new_params = structuredClone(params);
+ new_params.pos[1] = parseFloat(e.target.value);
+ on_change(new_params);
+ }
+
+ function onFontSizeChange(e)
+ {
+ let new_params = structuredClone(params);
+ new_params.font_size = parseFloat(e.target.value);
+ on_change(new_params);
+ }
+
+ function onColorChange(e)
+ {
+ let new_params = structuredClone(params);
+ new_params.color = e.target.value;
+ on_change(new_params);
+ }
+
+ return h(preact.Fragment, {},
+ h("div", {"class": "ButtonRowLeft"},
+ h("label", {}, "Content"),
+ h("input", {type: "text", value: params.content,
+ onchange: onContentChange}),
+ h("label", {}, "Color"),
+ h("input", {type: "color",
+ value: params.color,
+ onchange: onColorChange})),
+ h("div", {"class": "ButtonRowLeft"},
+ h("label", {}, "x"),
+ h("input", {type: "number", value: params.pos[0],
+ onchange: onPosXChange}),
+ h("label", {}, "y"),
+ h("input", {type: "number", value: params.pos[1],
+ onchange: onPosYChange}),
+ h("label", {}, "Font size"),
+ h("input", {type: "number", value: params.font_size,
+ min: 0, max: 88, step: 0.5,
+ onchange: onFontSizeChange})));
+}
+
+function EditorViewNetscape({app_state, on_state_change})
+{
+ const params = app_state.button_params;
+ function onLogoURLChange(e)
+ {
+ let new_state = structuredClone(app_state);
+ new_state.button_params.logo_url = e.target.value;
+ on_state_change(new_state);
+ }
+
+ function onBGColorChange(e)
+ {
+ let new_state = structuredClone(app_state);
+ new_state.button_params.color_bg = e.target.value;
+ on_state_change(new_state);
+ }
+
+ function onTextTopChange(new_params)
+ {
+ let new_state = structuredClone(app_state);
+ new_state.button_params.text_top = new_params;
+ on_state_change(new_state);
+ }
+
+ function onTextBottomChange(new_params)
+ {
+ let new_state = structuredClone(app_state);
+ new_state.button_params.text_bottom = new_params;
+ on_state_change(new_state);
+ }
+
+ function onRibbonStyleChange(e)
+ {
+ if(app_state.button_params.banner.style == e.target.value)
+ {
+ return;
+ }
+ let new_state = structuredClone(app_state);
+ let banner = new_state.button_params.banner;
+ banner.style = e.target.value;
+
+ // With the style change, the text also needs adjustments. We
+ // will just set it to the style’s default.
+ if(banner.style == "corner_bottom_right")
+ {
+ banner.text.pos = [65, 46];
+ }
+ else if(banner.style == "right")
+ {
+ banner.text.pos = [44, 54];
+ }
+
+ on_state_change(new_state);
+ }
+
+ function onRibbonColorChange(e)
+ {
+ let new_state = structuredClone(app_state);
+ new_state.button_params.banner.color = e.target.value;
+ on_state_change(new_state);
+ }
+
+ function onRibbonTextChange(new_text)
+ {
+ let new_state = structuredClone(app_state);
+ new_state.button_params.banner.text = new_text;
+ on_state_change(new_state);
+ }
+
+ return h("div", {},
+ h("div", {"class": "ButtonRowLeft"},
+ h("label", {}, "Background color"),
+ h("input", {type: "color",
+ value: params.color_bg,
+ onchange: onBGColorChange})),
+ h("div", {"class": "ButtonRowLeft"},
+ h("label", {}, "Logo URL"),
+ h("input", {type: "url", value: params.logo_url,
+ onchange: onLogoURLChange})),
+ h("div", {"class": "LabeledPanel"},
+ h("h2", {}, "Main text"),
+ h(TextControlView, {params: params.text_top,
+ on_change: onTextTopChange})),
+ h("div", {"class": "LabeledPanel"},
+ h("h2", {}, "Flavor text"),
+ h(TextControlView, {params: params.text_bottom,
+ on_change: onTextBottomChange})),
+ h("div", {"class": "LabeledPanel"},
+ h("h2", {}, "Ribbon"),
+ h("div", {"class": "ButtonRowLeft"},
+ h("label", {}, "Style"),
+ h("select", {value: params.banner.style,
+ onchange: onRibbonStyleChange},
+ h("option", {value: "corner_bottom_right"}, "Bottom right"),
+ h("option", {value: "right"}, "Vertical right")),
+ h("label", {}, "Background color"),
+ h("input", {type: "color",
+ value: params.banner.color,
+ onchange: onRibbonColorChange})),
+ h("div", {"class": "LabeledPanel"},
+ h("h2", {}, "Text"),
+ h(TextControlView, {params: params.banner.text,
+ on_change: onRibbonTextChange})),
+
+ ),
+
+
+ );
+}
+
+function EditorView({app_state})
+{
+ const [state, setState] = preactHooks.useState(app_state);
+ let preview_view = null;
+ let editor_view = null;
+ if(app_state.button_type == "netscape")
+ {
+ preview_view = ButtonViewNetscape;
+ editor_view = EditorViewNetscape;
+ }
+
+ function onStateChange(new_state)
+ {
+ console.debug("New state is", new_state);
+ setState(new_state);
+ }
+
+ return h("div", {},
+ h(preview_view, {app_state: state}),
+ h("select", {},
+ h("option", {value: "netscape"}, "Netscape"),
+ ),
+ h(editor_view, {app_state: state, on_state_change: onStateChange}));
+}
+
+function App()
+{
+ return h("div", {},
+ h(EditorView, {app_state: DEFAULT_APP_STATE}));
+}
+
+preact.render(h(App, {}), document.getElementById("App"));
diff --git a/button-maker/scripts/tiles.js b/button-maker/scripts/tiles.js
new file mode 100644
index 0000000..f35887a
--- /dev/null
+++ b/button-maker/scripts/tiles.js
@@ -0,0 +1,94 @@
+const TILES = [
+ null,
+ {
+ id: "blank",
+ name: "black",
+ inner_svg: null,
+ }, {
+ id: "teleporter",
+ name: "teleporter",
+ inner_svg: h("circle", {
+ "cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5,
+ "r": (CELL_SIZE - 10) * 0.5, "stroke": "black",
+ "fill": "transparent", "stroke-width": 6,
+ }),
+ }, {
+ id: "fleet_command",
+ name: "fleet command",
+ inner_svg: h("polyline", {
+ "points": `3, ${CELL_SIZE} \
+3, ${CELL_SIZE - 15} \
+13, ${CELL_SIZE - 25} \
+13, ${CELL_SIZE - 5} \
+${CELL_SIZE - 13}, ${CELL_SIZE - 5} \
+${CELL_SIZE - 13}, ${CELL_SIZE - 25} \
+${CELL_SIZE - 3}, ${CELL_SIZE - 15} \
+${CELL_SIZE - 3}, ${CELL_SIZE}`,
+ "fill": "black", "stroke-width": 0,
+ }),
+ }, {
+ id: "scanner",
+ name: "scanner",
+ inner_svg:
+ h(preact.Fragment, {},
+ h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5,
+ "r": CELL_SIZE * 0.5 - 2, "fill": "transparent",
+ "stroke": "black", "stroke-width": 2}),
+ h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5,
+ "r": CELL_SIZE * 0.25, "fill": "transparent",
+ "stroke": "black", "stroke-width": 2}),
+ h("line", {"x1": 2, "y1": CELL_SIZE * 0.5,
+ "x2": CELL_SIZE - 2, "y2": CELL_SIZE * 0.5,
+ "stroke": "black", "stroke-width": 2}),
+ h("line", {"y1": 2, "x1": CELL_SIZE * 0.5,
+ "y2": CELL_SIZE - 2, "x2": CELL_SIZE * 0.5,
+ "stroke": "black", "stroke-width": 2}),
+ h("line", {"x1": CELL_SIZE * 0.5, "y1": CELL_SIZE * 0.5,
+ "x2": CELL_SIZE * 0.5 + (CELL_SIZE * 0.5 - 2) * Math.cos(Math.PI / 6),
+ "y2": CELL_SIZE * 0.5 - (CELL_SIZE * 0.5 - 2) * Math.sin(Math.PI / 6),
+ "stroke": "black", "stroke-width": 2}),
+ h("circle", {"cx": 22, "cy": 6, "r": 2, "fill": "black",
+ "stroke-width": 0}),
+ h("circle", {"cx": 10, "cy": 22, "r": 4, "fill": "black",
+ "stroke-width": 0}),
+
+ ),
+ }, {
+ id: "extractor",
+ name: "stellar extractor",
+ inner_svg:
+ h(preact.Fragment, {},
+ h("circle", {"cx": CELL_SIZE * 0.5, "cy": 21,
+ "r": 10, "fill": "black", "stroke-width": 0}),
+ h("polygon", {"points": `${CELL_SIZE * 0.5 - 4}, 20 \
+${CELL_SIZE * 0.5 - 4}, 10 \
+${CELL_SIZE * 0.5 - 8}, 10 \
+${CELL_SIZE * 0.5}, 3 \
+${CELL_SIZE * 0.5 + 8}, 10 \
+${CELL_SIZE * 0.5 + 4}, 10 \
+${CELL_SIZE * 0.5 + 4}, 20`,
+ "stroke": "black", "stroke-width": 2,
+ "fill": "white"}),
+ ),
+ }, {
+ id: "storage",
+ name: "storage",
+ inner_svg:
+ h(preact.Fragment, {},
+ h("rect", {"x": 4, "y": 4, "width": CELL_SIZE - 8,
+ "height": CELL_SIZE - 8, "rx": 2, "ry": 2,
+ "fill": "black", "stroke-width": 0}),
+ h("line", {"x1": 4, "y1": 14, "x2": CELL_SIZE - 4, "y2": 14,
+ "stroke": "white", "stroke-width": 2}),
+ h("rect", {"x": CELL_SIZE * 0.5 - 4, "y": 10, "width": 8, "height": 8,
+ "fill": "white", "stroke-width": 0}),
+ ),
+ }, {
+ id: "plant",
+ name: "double cultivation chamber",
+ inner_svg: h("path", {"d": "m3.02 17.7a13 13 0 0013 13 13 13 0 00-13-13m13 13a13 13 0 0013-13 13 13 0 00-13 13m8.66-27.4v7.21a8.66 8.66 0 01-8.66 8.66 8.66 8.66 0 01-8.66-8.66v-7.21c1.07 0 2.12.173 3.12.563.793.332 1.5.821 2.09 1.44l3.43-3.43 3.43 3.43c.591-.622 1.3-1.11 2.09-1.44.996-.388 2.05-.563 3.12-.563z",
+ "fill": "black", "stroke-width": 0}),
+ }
+];
+
+const TILE_MAP = Object.fromEntries(TILES.slice(1).map(t => [t.id, t]));
diff --git a/button-maker/styles.css b/button-maker/styles.css
new file mode 100644
index 0000000..bee2d4b
--- /dev/null
+++ b/button-maker/styles.css
@@ -0,0 +1,326 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: unset;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+/* ========== Actual styles ======================================> */
+
+:root
+{
+ --color-bg: #c0c0c0;
+ --color-fg: black;
+ --color-border-dark-1: #808080;
+ --color-border-dark-2: black;
+ --color-border-light-1: #dfdfdf;
+ --color-border-light-2: white;
+ --color-titlebar-1: navy;
+ --color-titlebar-2: #1084d0;
+}
+
+*
+{
+ box-sizing: border-box;
+}
+
+html
+{
+ background: #008080;
+ color: var(--color-fg);
+ font-family: sans-serif;
+ font-size: 12px;
+ padding: 12px;
+}
+
+.Window
+{
+ box-shadow: inset -1px -1px var(--color-border-dark-2),
+ inset 1px 1px var(--color-border-light-1),
+ inset -2px -2px var(--color-border-dark-1),
+ inset 2px 2px var(--color-border-light-2);
+ background: var(--color-bg);
+ padding: 10px;
+
+ .Titlebar
+ {
+ background: linear-gradient(90deg, var(--color-titlebar-1), var(--color-titlebar-2));
+ margin: -6px -6px 10px -6px;
+ color: white;
+ padding: 3px;
+ }
+}
+
+.Dialog
+{
+ max-width: 910px;
+ margin: 0px auto;
+}
+
+.LabeledPanel
+{
+ border-top: solid var(--color-border-light-1) 1px;
+ border-right: solid var(--color-border-dark-1) 1px;
+ border-bottom: solid var(--color-border-dark-1) 1px;
+ border-left: solid var(--color-border-light-1) 1px;
+
+ box-shadow: 1px 1px var(--color-border-light-1),
+ -1px -1px var(--color-border-dark-1);
+ padding: 1ex;
+ padding-top: 0px;
+ margin: 2ex 0px;
+}
+
+.LabeledPanel > h3, .LabeledPanel > h2
+{
+ font-weight: normal;
+ display: inline-block;
+ position: relative;
+ top: calc(-0.5em);
+ left: 1ex;
+ background-color: var(--color-bg);
+ padding: 0px 1ex 0px 1ex;
+ margin: 0px;
+}
+
+input[type="number"]
+{
+ appearance: none;
+ border: none;
+ outline: none;
+ box-shadow: inset -1px -1px var(--color-border-light-2),
+ inset 1px 1px var(--color-border-dark-1),
+ inset -2px -2px var(--color-border-light-1),
+ inset 2px 2px var(--color-border-dark-2);
+ padding: 4px 0px 4px 6px;
+ font-family: monospace;
+ font-size: 0.9rem;
+}
+
+input[type="text"], input[type="url"], input[type="email"], textarea
+{
+ appearance: none;
+ border: none;
+ outline: none;
+ box-shadow: inset -1px -1px var(--color-border-light-2),
+ inset 1px 1px var(--color-border-dark-1),
+ inset -2px -2px var(--color-border-light-1),
+ inset 2px 2px var(--color-border-dark-2);
+ width: 100%;
+ padding: 4px 6px;
+ font-family: monospace;
+ font-size: 0.9rem;
+}
+
+a.Button, a.FloatButton
+{
+ color: black;
+ text-decoration: none;
+}
+
+button, input[type="submit"], input[type="color"]
+{
+ appearance: none;
+ border: none;
+ border-radius: 0px;
+ outline: none;
+ box-shadow: inset -1px -1px var(--color-border-dark-2),
+ inset 1px 1px var(--color-border-light-1),
+ inset -2px -2px var(--color-border-dark-1),
+ inset 2px 2px var(--color-border-light-2);
+ background: var(--color-bg);
+ text-align: center;
+}
+
+button, input[type="submit"]
+{
+ min-height: 23px;
+ min-width: 75px;
+ padding: 0 12px;
+}
+
+input[type="color"]
+{
+ padding: 4px;
+ width: 3em;
+ height: 2em;
+}
+
+.IconButton
+{
+ padding: 4px;
+ min-width: unset;
+ min-height: unset;
+}
+
+.FloatButton
+{
+ padding: 4px;
+ min-width: unset;
+ min-height: unset;
+ box-shadow: none;
+}
+
+.FloatButton:hover
+{
+ padding: 4px;
+ min-width: unset;
+ min-height: unset;
+ box-shadow: inset 1px 1px var(--color-border-light-1),
+ inset -1px -1px var(--color-border-dark-1);
+}
+
+button:active, .FloatButton:active
+{
+ box-shadow: inset -1px -1px var(--color-border-light-2),
+ inset 1px 1px var(--color-border-dark-1),
+ inset -2px -2px var(--color-border-light-1),
+ inset 2px 2px var(--color-border-dark-2);
+}
+
+hr
+{
+ border: none;
+ border-top: solid var(--color-border-dark-1) 1px;
+ border-bottom: solid var(--color-border-light-1) 1px;
+}
+
+.ButtonRow, .ButtonRowLeft
+{
+ display: flex;
+ gap: 10px;
+ margin: 10px 0;
+ align-items: baseline;
+}
+
+.ButtonRow
+{
+ justify-content: right;
+}
+
+.Toolbar
+{
+ display: flex;
+ border-bottom: solid var(--color-border-dark-1) 1px;
+ box-shadow: 0px 1px var(--color-border-light-1);
+ position: relative;
+ left: -10px;
+ top: -10px;
+ width: calc(100% + 2ex);
+ padding: 2px 8px;
+}
+
+table.InputFields
+{
+ border-collapse: collapse;
+ width: 100%;
+ table-layout: auto;
+
+ td
+ {
+ padding: 2px 2px;
+ }
+
+ td:nth-child(2)
+ {
+ width: 100%;
+ }
+
+}
+
+.StatusBar
+{
+ margin: 10px -6px -7px -6px;
+ display: flex;
+}
+
+.StatusCell
+{
+ box-shadow: inset -2px -2px var(--color-border-light-2),
+ inset 2px 2px var(--color-border-dark-1);
+ padding: 4px;
+ flex-grow: 1;
+}
+
+#Links
+{
+ box-shadow: inset -1px -1px var(--color-border-light-2),
+ inset 1px 1px var(--color-border-dark-1),
+ inset -2px -2px var(--color-border-light-1),
+ inset 2px 2px var(--color-border-dark-2);
+ padding: 2px;
+}
+
+table.TableView
+{
+ background-color: white;
+ border-collapse: collapse;
+ width: 100%;
+
+ thead th
+ {
+ box-shadow: inset -1px -1px var(--color-border-dark-2),
+ inset 1px 1px var(--color-border-light-1),
+ inset -2px -2px var(--color-border-dark-1),
+ inset 2px 2px var(--color-border-light-2);
+ background-color: var(--color-bg);
+ padding: 2px 4px 4px 4px;
+ text-align: left;
+ }
+
+ td
+ {
+ padding: 2px;
+ }
+}
+
+/* ========== App-specific =======================================> */
+
+#MainWindow
+{
+ max-width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+}