diff --git a/web/assets/css/about.css b/web/assets/css/about.css
new file mode 100755
index 0000000..8d95e00
--- /dev/null
+++ b/web/assets/css/about.css
@@ -0,0 +1,51 @@
+.service-up::after { content: '●'; }
+
+.service-down::after { content: '○'; }
+
+h3:is(.service-up, .service-down)::after {
+ font-size: 1rem;
+ margin-right: -.9rem;
+ vertical-align: text-top;
+ padding-left: .2rem; }
+
+.columns {
+ column-count: 3;
+ column-fill: balance-all;
+ column-gap: 1rem;
+ column-span: none; }
+
+.columns.figs { column-count: 2; }
+
+.columns > div {
+ display: inline-block;
+ width: 100%; }
+
+.columns h3 { color: var(--primary-color); }
+
+figure figcaption { font-size: .8rem; }
+
+figure.center,
+figure figcaption { text-align: center; }
+
+figure img { width: 100%; }
+
+figure.center img { width: 60%; }
+
+#contacts,
+#donation {
+ text-align: center;
+ width: 100%;
+ word-wrap: break-word; }
+
+:is(#contacts, #donation) :is(a, span) { margin: .3rem; }
+
+#contacts span a { margin: 0; }
+
+.banners {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center; }
+
+@media screen and (max-width: 640px) {
+ .columns,
+ .columns.figs { column-count: 1; } }
\ No newline at end of file
diff --git a/web/assets/css/articles.css b/web/assets/css/articles.css
new file mode 100755
index 0000000..7261a68
--- /dev/null
+++ b/web/assets/css/articles.css
@@ -0,0 +1,64 @@
+@font-face {
+ font-family: 'Share Tech Mono';
+ font-style: normal;
+ font-weight: 400;
+ src: local('ShareTechMono'), local('ShareTechMono-Regular'),
+ url(/assets/fonts/ShareTechMono-Regular.ttf) format('truetype'); }
+
+
+h3 {
+ font-size: 1.1rem;
+ font-variant: normal;
+ text-align: left; }
+
+h4,
+h5 {
+ text-indent: 1.5rem;
+ margin: 1rem 0 1rem 0; }
+
+:is(h3, h4, h5) a { color: var(--text-color); }
+
+:is(h3, h4, h5) a:hover { color: var(--primary-color); }
+
+code,
+pre {
+ font-family: 'Share Tech Mono';
+ font-size: 1.1rem;
+ letter-spacing: -1px; }
+
+pre {
+ border-left: 1px solid var(--primary-color);
+ padding-left: .25rem;
+ margin: .5rem 0;
+ overflow: scroll;
+ white-space: pre-wrap;
+ word-break: break-all; }
+
+figure { margin: 1rem 0; }
+
+figure.center { text-align: center; }
+
+article {
+ margin-top: 1rem;
+ max-width: 100%; }
+
+article:last-child { margin-bottom: 1rem; }
+
+article header {
+ display: flex;
+ flex-direction: column; }
+
+article header .menu {
+ display: flex;
+ font-size: .8rem;
+ justify-content: space-between; }
+
+article header nav a { font-variant: normal; }
+
+article header nav ol {
+ counter-reset: item;
+ list-style-type: none; }
+
+article header nav ol > li { counter-increment: item; }
+
+article header nav ol > li:before { content: counters(item, '.') '. '; }
\ No newline at end of file
diff --git a/web/assets/css/guestbook.css b/web/assets/css/guestbook.css
new file mode 100755
index 0000000..6503a67
--- /dev/null
+++ b/web/assets/css/guestbook.css
@@ -0,0 +1,90 @@
+::placeholder { color: var(--primary-color); }
+
+small { font-size: .8rem; }
+
+form {
+ display: grid;
+ gap: .5rem;
+ grid-template-areas:
+ "n w w"
+ "m m m"
+ "c a s";
+ grid-template-columns: 1fr 1fr 1fr; }
+
+:is(input, textarea):focus {
+ outline-color: var(--primary-color);
+ outline-style: solid;
+ outline-width: 1px; }
+
+input[name="name"] { grid-area: n; }
+
+input[name="website"] { grid-area: w; }
+
+textarea {
+ grid-area: m;
+ height: 5.5rem;
+ max-width: 100%;
+ min-width: 100%; }
+
+input[type="text"],
+textarea {
+ background-color: var(--background-color);
+ border: none;
+ border-bottom: 1px solid var(--primary-color);
+ color: var(--text-color);
+ font: inherit; }
+
+.checkboxes { grid-area: c; }
+
+label { padding-left: .5rem; }
+
+button.refresh:disabled {
+ color: gray;
+ cursor: progress; }
+
+.captcha {
+ align-items: center;
+ column-gap: .5rem;
+ display: flex;
+ grid-area: a;
+ justify-content: space-evenly; }
+
+.captcha img {
+ height: 40px;
+ width: 160px; }
+
+.captcha input[type="text"] {
+ height: 27px;
+ text-align: center;
+ width: 4.5rem; }
+
+input[type="submit"] {
+ background-color: var(--primary-color);
+ border: none;
+ color: #f5f5f5;
+ font: inherit;
+ grid-area: s;
+ height: 2rem; }
+
+input[type="submit"]:hover {
+ background-color: var(--secondary-color);
+ cursor: pointer; }
+
+article:not(:last-child) { margin-bottom: 1rem; }
+
+article header {
+ display: inline;
+ font-size: .85rem; }
+
+article > *,
+article div.reply > * { margin-left: .5rem; }
+
+article p.quote { font-style: italic; }
+
+#pagination a:not(:first-child) { margin-left: .5rem; }
+
+@media screen and (max-width: 641px) {
+ form {
+ gap: 1.5rem;
+ display: flex;
+ flex-direction: column; } }
\ No newline at end of file
diff --git a/web/assets/css/index.css b/web/assets/css/index.css
new file mode 100755
index 0000000..60250a7
--- /dev/null
+++ b/web/assets/css/index.css
@@ -0,0 +1,22 @@
+body {
+ height: 75vh;
+ margin-top: 25vh; }
+
+header { position: relative; }
+
+#logo { width: 100%; }
+
+nav,
+#services {
+ text-align: center;
+ width: 100%; }
+
+nav a:last-child { margin-left: .6rem; }
+
+
+#services :is(a, span) { margin: .3rem; }
+
+#services span a { margin: 0; }
+
+@media screen and (max-width: 640px) {
+ #services { display: flex; flex-direction: column; } }
\ No newline at end of file
diff --git a/web/assets/css/main.css b/web/assets/css/main.css
new file mode 100755
index 0000000..a962ce0
--- /dev/null
+++ b/web/assets/css/main.css
@@ -0,0 +1,156 @@
+@font-face {
+ font-family: 'Roboto Condensed';
+ font-style: normal;
+ font-weight: 400;
+ src: local('RobotoCondensed'), local('RobotoCondensed-Regular'),
+ url(/assets/fonts/RobotoCondensed-Regular.ttf); }
+
+:root {
+ --background-color: #0a0a0a;
+ --background-image: url('/assets/img/alpha1918_z.webp');
+ --primary-color: #cd2682;
+ --secondary-color: #9f2b68;
+ --text-color: #f5f5f5;
+ --text-indent: 1.6rem;
+ scrollbar-color: var(--primary-color) var(--background-color); }
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --background-color: #f5f5f5;
+ --primary-color: #9f2b68;
+ --secondary-color: #cd2682;
+ --text-color: #0a0a0a; } }
+
+* { margin: 0; }
+
+::selection {
+ background-color: var(--secondary-color);
+ color: var(--background-color); }
+
+a,
+button {
+ color: var(--primary-color);
+ text-decoration: none;
+ word-wrap: break-word; }
+
+:is(a, button):hover {
+ color: var(--secondary-color);
+ text-decoration: underline dotted;
+ cursor: pointer;
+ transition: .5s; }
+
+button {
+ background: none;
+ border: none;
+ font: inherit;
+ padding: 0; }
+
+p {
+ text-align: justify;
+ line-height: var(--text-indent);
+ text-indent: var(--text-indent); }
+
+p:not(:last-child) { margin-bottom: .1rem; }
+
+p.center {
+ text-align: center;
+ text-indent: 0; }
+
+h1 {
+ font-size: 1.8rem;
+ text-align: center;
+ color: var(--secondary-color);
+ margin: 0; }
+
+h1,
+h2 { font-variant: small-caps; }
+
+h2 { font-size: 1.4rem; }
+
+h2,
+h3 {
+ text-align: center;
+ margin: 1rem; }
+
+h3 { font-size: 1.05rem; }
+
+
+table {
+ border-collapse: collapse;
+ margin-bottom: .5rem; }
+
+tr :is(th, td) {
+ text-align: left;
+ vertical-align: top; }
+
+tr :is(th, td):not(:last-child) { padding-right: .5rem; }
+
+.highlighted { color: var(--primary-color); }
+
+html {
+ margin-left: calc(100vw - 100%);
+ margin-right: 0; }
+
+body {
+ background-color: var(--background-color);
+ color: var(--text-color);
+ font-family: 'Roboto Condensed', Roboto, sans-serif;
+ font-size: 1.1rem;
+ margin: 0 auto;
+ max-width: 960px;
+ width: 98%; }
+
+@media screen and (min-width: 1280px) {
+ body {
+ background-image: var(--background-image);
+ background-size: min(calc((100vw - 960px)/2), 25vw);
+ background-repeat: no-repeat;
+ background-position: bottom left;
+ }
+}
+
+header {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between; }
+
+#logo {
+ display: block;
+ width: 360px; }
+
+#logo text { fill: var(--text-color); }
+
+#logo .logo {
+ font-size: 2rem;
+ font-variant-caps: small-caps;
+ font-weight: bold; }
+
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ #logo .logo { font-size: 2.082rem; } }
+
+@-moz-document url-prefix() {
+ #logo .logo { font-size: 2rem; } }
+
+#logo .under { font-size: .88rem; }
+
+nav { margin-top: .5rem; }
+
+nav a { font-variant: small-caps; }
+
+nav a:not(:first-child) { margin-left: .6rem; }
+
+section {
+ margin-top: 1rem;
+ max-width: 100%; }
+
+footer {
+ font-size: .8rem;
+ text-align: center;
+ padding: 1rem 0; }
+
+@media screen and (max-width: 640px) {
+ body > header { display: block; }
+
+ #logo { width: 100%; }
+
+ body > header nav { text-align: center; } }
\ No newline at end of file
diff --git a/web/assets/css/mindflow.css b/web/assets/css/mindflow.css
new file mode 100755
index 0000000..fc61cf3
--- /dev/null
+++ b/web/assets/css/mindflow.css
@@ -0,0 +1,9 @@
+.hidden { display: none; }
+
+button:not(:last-child) { padding-right: 1rem; }
+
+article header a { color: var(--text-color); }
+
+article footer {
+ text-align: right;
+ padding: 0; }
\ No newline at end of file
diff --git a/web/assets/fonts/ShareTechMono-Regular.ttf b/web/assets/fonts/ShareTechMono-Regular.ttf
new file mode 100755
index 0000000..c8e530f
Binary files /dev/null and b/web/assets/fonts/ShareTechMono-Regular.ttf differ
diff --git a/web/assets/img/acer.webp b/web/assets/img/acer.webp
new file mode 100755
index 0000000..e83f1f2
Binary files /dev/null and b/web/assets/img/acer.webp differ
diff --git a/web/assets/img/acer_thumb.webp b/web/assets/img/acer_thumb.webp
new file mode 100755
index 0000000..5702c16
Binary files /dev/null and b/web/assets/img/acer_thumb.webp differ
diff --git a/web/assets/img/alpha1918_z.webp b/web/assets/img/alpha1918_z.webp
new file mode 100755
index 0000000..b3fa176
Binary files /dev/null and b/web/assets/img/alpha1918_z.webp differ
diff --git a/web/assets/img/articles/mikrotik_please_just_dont.jpg b/web/assets/img/articles/mikrotik_please_just_dont.jpg
new file mode 100755
index 0000000..c948efb
Binary files /dev/null and b/web/assets/img/articles/mikrotik_please_just_dont.jpg differ
diff --git a/web/assets/img/banner_240x60.gif b/web/assets/img/banner_240x60.gif
new file mode 100755
index 0000000..86420f8
Binary files /dev/null and b/web/assets/img/banner_240x60.gif differ
diff --git a/web/assets/img/banner_88x31.gif b/web/assets/img/banner_88x31.gif
new file mode 100755
index 0000000..31bd091
Binary files /dev/null and b/web/assets/img/banner_88x31.gif differ
diff --git a/web/assets/img/banner_dark_240x60.gif b/web/assets/img/banner_dark_240x60.gif
new file mode 100755
index 0000000..4e7f9d6
Binary files /dev/null and b/web/assets/img/banner_dark_240x60.gif differ
diff --git a/web/assets/img/banner_dark_88x31.gif b/web/assets/img/banner_dark_88x31.gif
new file mode 100755
index 0000000..7211523
Binary files /dev/null and b/web/assets/img/banner_dark_88x31.gif differ
diff --git a/web/assets/img/favicon.ico b/web/assets/img/favicon.ico
new file mode 100755
index 0000000..2fef958
Binary files /dev/null and b/web/assets/img/favicon.ico differ
diff --git a/web/assets/img/favicon.svg b/web/assets/img/favicon.svg
new file mode 100755
index 0000000..3386c7f
--- /dev/null
+++ b/web/assets/img/favicon.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/web/assets/img/favicon_128.png b/web/assets/img/favicon_128.png
new file mode 100755
index 0000000..c7d066e
Binary files /dev/null and b/web/assets/img/favicon_128.png differ
diff --git a/web/assets/img/my_cat.webp b/web/assets/img/my_cat.webp
new file mode 100755
index 0000000..3874fba
Binary files /dev/null and b/web/assets/img/my_cat.webp differ
diff --git a/web/assets/img/my_cat_2.webp b/web/assets/img/my_cat_2.webp
new file mode 100755
index 0000000..5158bb6
Binary files /dev/null and b/web/assets/img/my_cat_2.webp differ
diff --git a/web/assets/img/my_cat_2_thumb.webp b/web/assets/img/my_cat_2_thumb.webp
new file mode 100755
index 0000000..ff19f19
Binary files /dev/null and b/web/assets/img/my_cat_2_thumb.webp differ
diff --git a/web/assets/img/my_cat_thumb.webp b/web/assets/img/my_cat_thumb.webp
new file mode 100755
index 0000000..f42049f
Binary files /dev/null and b/web/assets/img/my_cat_thumb.webp differ
diff --git a/web/assets/img/raspi.webp b/web/assets/img/raspi.webp
new file mode 100755
index 0000000..dc114b6
Binary files /dev/null and b/web/assets/img/raspi.webp differ
diff --git a/web/assets/img/raspi_thumb.webp b/web/assets/img/raspi_thumb.webp
new file mode 100755
index 0000000..ceef936
Binary files /dev/null and b/web/assets/img/raspi_thumb.webp differ
diff --git a/web/assets/img/ts3_banner_960.png b/web/assets/img/ts3_banner_960.png
new file mode 100755
index 0000000..51a1355
Binary files /dev/null and b/web/assets/img/ts3_banner_960.png differ
diff --git a/web/assets/js/captcha_refresh.js b/web/assets/js/captcha_refresh.js
new file mode 100755
index 0000000..a7cb54a
--- /dev/null
+++ b/web/assets/js/captcha_refresh.js
@@ -0,0 +1,40 @@
+const g_captcha_timeout_seconds = 600;
+
+const e_captcha = document.getElementsByClassName("captcha")[0];
+
+let g_captcha_timeout_remain = g_captcha_timeout_seconds;
+let g_current_captcha_id = e_captcha.children[0].value;
+
+async function getNewCaptcha() {
+ const id = await fetch("/api/captcha/", { method: "POST"}).then(r => r.text());
+ g_current_captcha_id = e_captcha.children[0].value = id;
+ const style = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
+ setTimeout(() => { e_captcha.children[1].src = `/api/captcha/${id}/image?style=${style}` }, 600);
+ g_captcha_timeout_remain = g_captcha_timeout_seconds;
+}
+
+e_captcha.children[3].innerHTML =
+ `ed in 600 seconds.`;
+
+const captcha_refresh = document.getElementById("refresh");
+const captcha_remain = document.getElementById("remain");
+
+captcha_refresh.classList.toggle("refresh");
+captcha_refresh.addEventListener("click", async e => {
+ e.preventDefault();
+ e.target.disabled = true;
+ setTimeout(async () => {
+ e.target.disabled = false;
+ await fetch(`/api/captcha/${g_current_captcha_id}?remove`); }, 3000);
+ e_captcha.children[2].value = "";
+ await getNewCaptcha();
+});
+
+// Remove unused CAPTCHA on a server.
+window.addEventListener("unload", () => fetch(`/api/captcha/${g_current_captcha_id}?remove`));
+
+setInterval(async () => {
+ captcha_remain.innerText = --g_captcha_timeout_remain;
+ if (g_captcha_timeout_remain == 0)
+ await getNewCaptcha();
+}, 1000);
\ No newline at end of file
diff --git a/web/assets/js/mindflow.js b/web/assets/js/mindflow.js
new file mode 100755
index 0000000..9d33f8d
--- /dev/null
+++ b/web/assets/js/mindflow.js
@@ -0,0 +1,12 @@
+const articles = document.getElementsByTagName("article");
+
+document.getElementById("filter").classList.remove("hidden");
+
+function filter(e) {
+ for (const a of articles)
+ a.classList.toggle("hidden",
+ !(e.target.name === "all" || a.id.startsWith(e.target.name)));
+}
+
+for (const b of document.getElementsByTagName("button"))
+ b.addEventListener("click", filter);
\ No newline at end of file