summaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2024-07-13 18:04:05 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2024-07-13 18:11:42 -0400
commit81c4d517735983a5afd6e9dc800257c761598527 (patch)
tree6b924707914d385126e6d330d2c628fd26f23a27 /static
parent7f37518e4792f040a753a5d8d68d51e76cc0b2be (diff)
The `org' directory is no longer necessary
Diffstat (limited to 'static')
-rw-r--r--static/css/fonts.css59
-rw-r--r--static/css/highlight.css61
-rw-r--r--static/css/normalize.css349
-rw-r--r--static/css/style.css557
-rw-r--r--static/font/Cantarell-Bold.otfbin0 -> 69196 bytes
-rw-r--r--static/font/Cantarell-BoldOblique.otfbin0 -> 75888 bytes
-rw-r--r--static/font/Cantarell-Oblique.otfbin0 -> 74892 bytes
-rw-r--r--static/font/Cantarell-Regular.otfbin0 -> 68340 bytes
-rw-r--r--static/font/Hack-Bold.ttfbin0 -> 317628 bytes
-rw-r--r--static/font/Hack-BoldItalic.ttfbin0 -> 322288 bytes
-rw-r--r--static/font/Hack-Italic.ttfbin0 -> 316156 bytes
-rw-r--r--static/font/Hack-Regular.ttfbin0 -> 309408 bytes
-rw-r--r--static/gpg.txt87
-rw-r--r--static/image/cbt-2023-04-13.jpgbin0 -> 375364 bytes
-rw-r--r--static/image/cc-by-sa-4.0.pngbin0 -> 475 bytes
-rw-r--r--static/image/clock.pngbin0 -> 46036 bytes
-rw-r--r--static/image/default-icon.pngbin0 -> 545281 bytes
-rw-r--r--static/image/favicon.icobin0 -> 5430 bytes
-rw-r--r--static/image/hourglass.gifbin0 -> 21999 bytes
-rw-r--r--static/image/i2p.svg1
-rw-r--r--static/image/lambda.svg3
-rw-r--r--static/image/lobsters.pngbin0 -> 4535 bytes
-rw-r--r--static/image/oneko.gifbin0 -> 3316 bytes
-rw-r--r--static/image/opengraph-icon-coq.jpgbin0 -> 95670 bytes
-rw-r--r--static/image/portrait.pngbin0 -> 331269 bytes
-rw-r--r--static/image/profile-picture.jpgbin0 -> 69228 bytes
-rw-r--r--static/image/tango-emblem-important.pngbin0 -> 81310 bytes
-rw-r--r--static/image/tor.svg42
-rw-r--r--static/image/webmention-logo.pngbin0 -> 5579 bytes
-rw-r--r--static/js/comment-reaction.js143
-rw-r--r--static/js/dynamic-comment-form.js94
-rw-r--r--static/js/oneko.js182
-rw-r--r--static/js/proof-of-work.js109
-rw-r--r--static/js/rsvp.js206
-rw-r--r--static/js/section-folds.js66
-rw-r--r--static/js/webmention.js229
36 files changed, 2188 insertions, 0 deletions
diff --git a/static/css/fonts.css b/static/css/fonts.css
new file mode 100644
index 0000000..05a70c6
--- /dev/null
+++ b/static/css/fonts.css
@@ -0,0 +1,59 @@
+/* Cantarell */
+
+@font-face {
+ font-family: 'Cantarell';
+ src: url('/static/font/Cantarell-Bold.otf') format('opentype');
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Cantarell';
+ src: url('/static/font/Cantarell-BoldOblique.otf') format('opentype');
+ font-weight: bold;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Cantarell';
+ src: url('/static/font/Cantarell-Oblique.otf') format('opentype');
+ font-weight: normal;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Cantarell';
+ src: url('/static/font/Cantarell-Regular.otf') format('opentype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+/* Hack */
+
+@font-face {
+ font-family: 'Hack';
+ src: url('/static/font/Hack-Bold.ttf');
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Hack';
+ src: url('/static/font/Hack-BoldItalic.ttf');
+ font-weight: bold;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Hack';
+ src: url('/static/font/Hack-Italic.ttf');
+ font-weight: normal;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Hack';
+ src: url('/static/font/Hack-Regular.ttf');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/static/css/highlight.css b/static/css/highlight.css
new file mode 100644
index 0000000..1e8899f
--- /dev/null
+++ b/static/css/highlight.css
@@ -0,0 +1,61 @@
+.org-preprocessor,
+.org-builtin {
+ color: #afd7d7;
+}
+
+.org-comment-delimiter,
+.org-comment {
+ color: #ff8700;
+}
+
+.org-constant {
+ color: #87ffd7;
+}
+
+.org-function-name {
+ color: #afd7ff;
+}
+
+.org-keyword {
+ color: #1fb6d3;
+}
+
+.org-negation-char {
+ color: #ff4b4b;
+}
+
+.org-regexp-grouping-backslash {
+ color: #ffd787;
+}
+
+.org-regexp-grouping-construct {
+ color: #d18aff;
+}
+
+.org-doc,
+.org-doc-string,
+.org-string {
+ color: #ffaf87;
+}
+
+.org-type {
+ color: #87ff87;
+}
+
+.org-variable-name {
+ color: #ffd787;
+}
+
+.org-warning {
+ color: #dd0000;
+}
+
+
+span.org-diff-added { text-decoration: none; background: #c0f8d0; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; }
+span.org-diff-added a { background: #c0f8d0; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; }
+span.org-diff-indicator-added { text-decoration: none; background: #c0f8d0; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; }
+span.org-diff-indicator-added a { background: #c0f8d0; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; }
+span.org-diff-removed { text-decoration: none; background: #ffd8d5; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; }
+span.org-diff-removed a { background: #ffd8d5; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; }
+span.org-diff-indicator-removed { text-decoration: none; background: #ffd8d5; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; }
+span.org-diff-indicator-removed a { background: #ffd8d5; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; }
diff --git a/static/css/normalize.css b/static/css/normalize.css
new file mode 100644
index 0000000..192eb9c
--- /dev/null
+++ b/static/css/normalize.css
@@ -0,0 +1,349 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+ display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..52d19af
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,557 @@
+[hidden] { display: none !important; }
+
+html {
+ font-family: "Cantarell", sans-serif;
+}
+
+@media (max-width: 54em) {
+ html {
+ font-size: 14px;
+ line-height: 160%;
+ }
+}
+
+@media (max-width: 26em) {
+ html {
+ font-size: 12px;
+ line-height: 150%;
+ }
+}
+
+body {
+ max-width: 54rem;
+ margin: .5rem auto;
+ position: relative;
+}
+
+@media (max-width: 54em) {
+ body {
+ max-width: 100%;
+ padding: 0 1rem;
+ }
+}
+
+/* Top-level header. */
+
+body > header {
+ display: flex;
+ align-items: center;
+}
+
+@media screen and (max-width: 360px) {
+ body > header {
+ flex-direction: column;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ }
+}
+
+body > header > a > img {
+ max-width: 4rem;
+}
+
+@media screen and (min-width: 480px) {
+ body > header > a {
+ padding-right: 1rem;
+ }
+}
+
+body > header > nav ul {
+ list-style-type: none;
+ padding-left: 0;
+}
+
+@media screen and (min-width: 480px) {
+ body > header > nav > ul > li {
+ display: inline-block;
+ }
+
+ body > header > nav > ul li {
+ padding: 1rem 2rem;
+ border-bottom: 2px solid #d2d6dd;
+ text-align: center;
+ }
+
+ body > header > nav > ul li > a {
+ color: #000;
+ text-decoration: none;
+ }
+
+ body > header > nav > ul .drop-menu {
+ display: none;
+ position: absolute;
+ z-index: 1;
+ }
+
+ body > header > nav > ul .drop-menu > li {
+ background: #fff;
+ }
+
+ body > header > nav > ul > li:hover .drop-menu {
+ display: block;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ body > header > nav > ul .drop-menu {
+ list-style-type: circle;
+ padding-left: 1em;
+ }
+}
+
+/* Horizontal lists. */
+
+.horizontal-list {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+
+ list-style-type: none;
+ text-align: center;
+
+ margin: 0;
+ padding: 8px 0;
+}
+
+/* Pagination. */
+
+#pagination {
+ display: flex;
+ justify-content: space-around;
+}
+
+/* Tag clouds. */
+
+#tag-cloud a {
+ color: #000;
+}
+
+#tag-cloud li {
+ display: inline-block;
+ margin: 10px 5px;
+ padding: 5px 10px;
+ vertical-align: top;
+
+ text-align: center;
+
+ border-radius: 4px;
+ border-style: solid;
+ border-width: thin;
+}
+
+/* Links. */
+
+a:link, a:visited { color: #00E; text-decoration: none; }
+
+/* Figures */
+
+figure > img {
+ display: block;
+ max-height: 600px;
+ max-width: 90%;
+ margin: auto;
+
+ border-radius: 2px;
+}
+
+figure > .org-svg {
+ display: block;
+ max-width: 90%;
+ margin: auto;
+}
+
+figure > figcaption {
+ padding-top: 1rem;
+}
+
+/* Embeds. */
+
+.mastodon > iframe,
+.peertube > iframe {
+ display: block;
+ width: 90%;
+ min-height: 100%;
+ margin: auto;
+ border-radius: 4px;
+}
+
+/* Footnotes. */
+
+.footdef {
+ margin-bottom: 1rem;
+}
+
+.footdef > sup, .footpara {
+ display: inline;
+}
+
+/* Comments & Webmention. */
+
+textarea {
+ width: 100%;
+ height: 4rem;
+}
+
+textarea, input[type=text], input[type=url] {
+ border: 1px solid #ccc;
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+}
+
+#webmention p .author-name {
+ margin: 4px;
+}
+
+#webmention {
+ border-top: 2px solid #d2d6dd;
+}
+
+ul.webmention-container {
+ list-style-type: none;
+}
+
+ul.webmention-container > li {
+ border: 2px solid #d2d6dd;
+ border-bottom: none;
+}
+
+ul.webmention-container > li:last-child {
+ border-bottom: 2px solid #d2d6dd;
+}
+
+ul.webmention-container > li.comment {
+ padding: 0.5rem;
+}
+
+ul.webmention-container > li.comment img.u-photo {
+ width: 4rem;
+ padding: 0 1rem 1rem 0;
+ float: left;
+ clear: both;
+ height: 100%;
+}
+
+ul.webmention-container > li.interaction {
+ padding: 4px;
+}
+
+ul.webmention-container > li.interaction img.u-photo {
+ max-width: 16px;
+ margin-right: 8px;
+ float: left;
+ clear: both;
+}
+
+ul.webmention-container ul.comment-reactions {
+ margin-top: 8px;
+}
+
+ul.webmention-container ul.comment-reactions li {
+ border-radius: 4px;
+ border: 1px solid #d2d6dd;
+ display: inline;
+ list-style-type: none;
+ padding: 4px;
+}
+
+/* Webmention (or comment) source indicator. */
+
+ul.webmention-container .comment {
+ position: relative;
+}
+
+ul.webmention-container .comment .comment-source-identifier {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ max-width: 16px;
+}
+
+/* Widget for comment reactions. */
+
+.emoji-picker {
+ width: 51%;
+ height: 2.75em;
+ border: solid 1px;
+ overflow: scroll;
+ margin-top: 16px;
+}
+
+.emoji-picker > span {
+ margin: 2px;
+ font-size: 2em;
+}
+
+
+/* Webmention form. */
+
+form#webmention-form {
+ border-top: 2px solid #d2d6dd;
+ padding-top: 1rem;
+}
+
+#webmention-input-group {
+ margin-top: 1rem;
+ display: grid;
+ grid-template-columns: 9fr 1fr;
+}
+
+/* Comment form. */
+
+.required:after {
+ content:" *";
+ color: red;
+}
+
+fieldset#comment-content {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ grid-gap:5px;
+}
+
+#comment-form #captcha-trigger-block label {
+ display: block;
+ margin-bottom: 16px;
+}
+
+#comment-form img {
+ max-width: 100%;
+}
+
+/* Source code. */
+
+pre {
+ color: #f0e9dc;
+ font-family: "Hack", monospace;
+ font-size: 90%;
+
+ background-color: #303539;
+ border-radius: 4px;
+ padding: 1rem;
+ overflow-x: auto;
+}
+
+/* Footer. */
+
+body > footer {
+ margin-top: 1.2rem;
+ border-top: 2px solid #d2d6dd;
+}
+
+body > footer > div {
+ margin-top: 1rem;
+ display: flex;
+ align-items: center;
+}
+
+body > footer > div > p {
+ margin: 0;
+ padding-right: 1rem;
+}
+
+/* About page. */
+
+.overline {
+ border-top: 2px solid #d2d6dd;
+ margin-top: 0.5rem;
+ overflow: auto; /* Not specific to overline. */
+}
+
+/* Tables. */
+
+table {
+ text-align: center;
+ width: 100%;
+}
+
+td {
+ padding: 0 1rem;
+}
+
+tr:nth-child(even) {
+ background-color: #f2f2f2;
+}
+
+/* Buttons. */
+
+.lang button {
+ display: inline;
+ padding: .3em .6em .3em;
+ font-size: 75%;
+ font-weight: 700;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: .25em;
+}
+
+.label-info {
+ background-color: #5bc0de;
+}
+
+.label-good {
+ background-color: #5cb85c;
+ border-color: #4cae4c;
+}
+
+.label-okay {
+ background-color: #ec971f;
+ border-color: #d58512;
+}
+
+.label-bad {
+ background-color: #dc3545;
+}
+
+/* Spoilered text. */
+
+.spoiler {
+ background-color: #2e3436;
+ color: #2e3436;
+}
+
+.spoiler:hover {
+ background-color: #f6f5f4;
+}
+
+/* Folded sections. */
+.fold-hidden {
+ height: 300px;
+ overflow: hidden;
+ mask-image: linear-gradient(to bottom, black 0%, black 80%, transparent 100%);
+}
+
+/* Styles specific to the "about" page. */
+
+.portrait {
+ max-width: 30%;
+ float: right;
+}
+
+.portrait > img {
+ max-width: 100%;
+}
+
+.project-section td:nth-child(1) {
+ width: 15%;
+}
+
+.project-section td:nth-child(2) {
+ width: 10%;
+}
+
+.project-section td:nth-child(3) {
+ width: 80px;
+}
+
+.project-section td:nth-child(4) {
+ width: 15%;
+}
+
+.team-timeline-section img {
+ max-width: 32px;
+}
+
+.team-timeline-section td:nth-child(1) {
+ width: 15%;
+}
+
+.team-timeline-section td:nth-child(2) {
+ width: 40px;
+}
+
+/* .team-timeline-section td:nth-child(3) { */
+/* width: 20%; */
+/* } */
+
+.team-timeline-section td:nth-child(4) {
+ width: 15%;
+}
+
+.team-timeline-section td:nth-child(5) {
+ width: 15%;
+}
+
+.lang-c {
+ background-color: #555555;
+ color: #fff;
+}
+
+.lang-cpp {
+ background-color: #f34b7d;
+ color: #fff;
+}
+
+.lang-python {
+ background-color: #3572a5;
+ color: #ffdb4e;
+}
+
+.lang-rust {
+ background-color: #dea584;
+ color: #fff;
+}
+
+.lang-kotlin {
+ background-color: #f89917;
+ color: #fff;
+}
+
+.lang-fennel {
+ background-color: #2ca02c;
+ color: #fff;
+}
+
+.lang-elisp {
+ background-color: #c065db;
+ color: #fff;
+}
+
+.lang-scheme {
+ background-color: #1e4aec;
+ color: #fff;
+}
+
+.lang-ebuild {
+ background-color: #4d4270;
+ color: #fff;
+}
+
+.article-front-matter {
+ border: 1px solid;
+ border-radius: 3px;
+ padding: 0.25em 1em;
+}
+
+.article-warning {
+ background-image: url('/static/image/tango-emblem-important.png');
+ background-repeat: no-repeat;
+ background-size: 4em 4em;
+ background-position: 1.5em 50%;
+ padding: 0.25em 1em 0.25em 7em;
+}
+
+.article-future-interjection {
+ background-image: url('/static/image/clock.png');
+ background-repeat: no-repeat;
+ background-size: 4em 4em;
+ background-position: 1.5em 50%;
+ padding: 0.25em 1em 0.25em 7em;
+}
+
+#loading {
+ border: 1px solid;
+ width: fit-content;
+ padding: 5px;
+ margin: auto;
+}
+
+fieldset {
+ padding: 12px;
+ margin: 15px;
+}
+
+#rsvp-input input {
+ margin: 0px 16px;
+}
diff --git a/static/font/Cantarell-Bold.otf b/static/font/Cantarell-Bold.otf
new file mode 100644
index 0000000..a5b620b
--- /dev/null
+++ b/static/font/Cantarell-Bold.otf
Binary files differ
diff --git a/static/font/Cantarell-BoldOblique.otf b/static/font/Cantarell-BoldOblique.otf
new file mode 100644
index 0000000..5f49d64
--- /dev/null
+++ b/static/font/Cantarell-BoldOblique.otf
Binary files differ
diff --git a/static/font/Cantarell-Oblique.otf b/static/font/Cantarell-Oblique.otf
new file mode 100644
index 0000000..70539ed
--- /dev/null
+++ b/static/font/Cantarell-Oblique.otf
Binary files differ
diff --git a/static/font/Cantarell-Regular.otf b/static/font/Cantarell-Regular.otf
new file mode 100644
index 0000000..fca6759
--- /dev/null
+++ b/static/font/Cantarell-Regular.otf
Binary files differ
diff --git a/static/font/Hack-Bold.ttf b/static/font/Hack-Bold.ttf
new file mode 100644
index 0000000..7ff4975
--- /dev/null
+++ b/static/font/Hack-Bold.ttf
Binary files differ
diff --git a/static/font/Hack-BoldItalic.ttf b/static/font/Hack-BoldItalic.ttf
new file mode 100644
index 0000000..3b137d9
--- /dev/null
+++ b/static/font/Hack-BoldItalic.ttf
Binary files differ
diff --git a/static/font/Hack-Italic.ttf b/static/font/Hack-Italic.ttf
new file mode 100644
index 0000000..d26198a
--- /dev/null
+++ b/static/font/Hack-Italic.ttf
Binary files differ
diff --git a/static/font/Hack-Regular.ttf b/static/font/Hack-Regular.ttf
new file mode 100644
index 0000000..92a90cb
--- /dev/null
+++ b/static/font/Hack-Regular.ttf
Binary files differ
diff --git a/static/gpg.txt b/static/gpg.txt
new file mode 100644
index 0000000..1de4e2a
--- /dev/null
+++ b/static/gpg.txt
@@ -0,0 +1,87 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+tSuc5igEcOo6
+=dfc8
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/static/image/cbt-2023-04-13.jpg b/static/image/cbt-2023-04-13.jpg
new file mode 100644
index 0000000..f691e45
--- /dev/null
+++ b/static/image/cbt-2023-04-13.jpg
Binary files differ
diff --git a/static/image/cc-by-sa-4.0.png b/static/image/cc-by-sa-4.0.png
new file mode 100644
index 0000000..003dd0a
--- /dev/null
+++ b/static/image/cc-by-sa-4.0.png
Binary files differ
diff --git a/static/image/clock.png b/static/image/clock.png
new file mode 100644
index 0000000..d35c884
--- /dev/null
+++ b/static/image/clock.png
Binary files differ
diff --git a/static/image/default-icon.png b/static/image/default-icon.png
new file mode 100644
index 0000000..832f3ef
--- /dev/null
+++ b/static/image/default-icon.png
Binary files differ
diff --git a/static/image/favicon.ico b/static/image/favicon.ico
new file mode 100644
index 0000000..842478f
--- /dev/null
+++ b/static/image/favicon.ico
Binary files differ
diff --git a/static/image/hourglass.gif b/static/image/hourglass.gif
new file mode 100644
index 0000000..e1bf222
--- /dev/null
+++ b/static/image/hourglass.gif
Binary files differ
diff --git a/static/image/i2p.svg b/static/image/i2p.svg
new file mode 100644
index 0000000..dfc85a8
--- /dev/null
+++ b/static/image/i2p.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><path fill="#fff" d="M0 0v512h512V0H0z"/><path fill="#010101" d="M153 239c-19.3 10-36 24.8-54.3 36.5-9.8 8.3-25.3 12.6-28.7 26 10.3 23 37.5 34 53.6 53 23.2 14.8 12.4 37.4 10.8 58.5-.6 19.8 8 38.2 19.5 53.8 0 14-32 35-4 41 32-.2 63 1.3 95 0 19-6.4 41 2.6 61-2.3 23-1.2 47-.3 70-2.6 23-16-28-31-1.6-47 14-18 10.7-42 5.5-62 18-15 18.3-37 20-58 5-20 14-39 19-59-4-24-39-42-46-57 18-29 27-64 20-98-6.6-55-51-101-105-113-60-15-128 17-155.5 72.5-16 31-18 68.4-9.4 102 5.6 20.4 17 38 29.6 54.4z"/><path fill="#576e57" d="M328 25l1 1z"/><path fill="#444b19" d="M267 26l1 1-1-1z"/><path fill="#ffff01" d="M180 329l18 8c9.2-17.5 14.8-38.4 21.6-57 2.3-6.3 3.5-17 9.5-21.2 5-3.3 15-.5 20 0 11 1 24 .7 34-1.7 53-11 90-57 92-111 3-73-69-132-141-117-44 10-76 44-88 86-10 38 2 79 28 107 7 8 15 14 24 20 3.8 3 10 5 11 10 1.4 6-4.3 14-6 19-6 20-16.4 39-21.2 59z"/><path fill="#444b19" d="M275 27l1 1-1-1m-39 1l1 1-1-1m-8 2l1 1-1-1z"/><path fill="#6c6c02" d="M291 31l1 1-1-1z"/><path fill="#989801" d="M207 38l1 1-1-1zm105 2l1 1-1-1z"/><path fill="#444b19" d="M313 40l1 1-1-1z"/><path fill="#6c6c02" d="M320 44l1 1-1-1z"/><path fill="#444b19" d="M321 45l1 1-1-1m-130 2l1 1-1-1z"/><path fill="#989801" d="M192 47l1 1-1-1zm-2 1l1 1-1-1z"/><path fill="#444b19" d="M326 48l1 1-1-1z"/><path fill="#989801" d="M327 49l1 1-1-1"/><path fill="#9fc79f" d="M359 50l1 1-1-1z"/><path fill="#989801" d="M186 51l1 1-1-1m-1 1l1 1-1-1m-1 1l1 1-1-1z"/><path fill="#6c6c02" d="M182 54l1 1-1-1m-1 1l1 1-1-1z"/><path fill="#9fc79f" d="M334 55l1 1-1-1z"/><path fill="#6c6c02" d="M180 56l1 1-1-1z"/><path fill="#989801" d="M179 57l1 1-1-1m-1 1l1 1-1-1m-1 1l1 1z"/><path fill="#6c6c02" d="M176 60l1 1-1-1m-1 1l1 1-1-1z"/><path fill="#989801" d="M341 61l1 1-1-1z"/><path fill="#6c6c02" d="M174 62l1 1-1-1z"/><path fill="#989801" d="M342 62l1 1-1-1z"/><path fill="#6c6c02" d="M173 63l1 1-1-1z"/><path fill="#989801" d="M343 63l1 1-1-1m-171 1l1 1-1-1m172 0l1 1-1-1m-173 1l1 1-1-1m-1 1l1 1-1-1z"/><path fill="#010101" d="M299 75.5c-52.4 11-36.2 92.8 16 81.7 52.7-11.2 36.3-92.8-16-81.7z"/><path fill="#444b19" d="M354 77l1 1-1-1z"/><path fill="#6c6c02" d="M161 78l1 1-1-1z"/><path fill="#010101" d="M203 78.4c-42 5.6-52.3 62.7-12 80 7.5 3 18 2.6 26 1.4 48.6-7.5 34.8-87.8-14-81.4z"/><path fill="#989801" d="M289 78l1 1-1-1m35 0l1 1-1-1z"/><path fill="#444b19" d="M355 78l1 1-1-1z"/><path fill="#989801" d="M221 80l1 1-1-1z"/><path fill="#9fc79f" d="M328 80l1 1-1-1z"/><path fill="#6c6c02" d="M356 80l1 1-1-1zm-74 2l1 1-1-1z"/><path fill="#444b19" d="M185 83l1 1-1-1z"/><path fill="#989801" d="M281 83l1 1-1-1z"/><path fill="#6c6c02" d="M358 83l1 1-1-1m-174 1l1 1-1-1z"/><path fill="#444b19" d="M228 84l1 1-1-1z"/><path fill="#989801" d="M280 84l1 1-1-1z"/><path fill="#444b19" d="M359 84l1 1-1-1z"/><path fill="#6c6c02" d="M182 85l1 1-1-1m48 0l1 1-1-1z"/><path fill="#444b19" d="M334 85l1 1-1-1z"/><path fill="#6c6c02" d="M359 85l1 1-1-1m-203 1l1 1-1-1m25 0l1 1-1-1m50 0l1 1-1-1z"/><path fill="#989801" d="M277 86l1 1-1-1z"/><path fill="#444b19" d="M305 86l1 1-1-1zm30 0l1 1-1-1zm-103 1l1 1-1-1z"/><path fill="#989801" d="M276 87l1 1-1-1z"/><path fill="#fff" d="M283 134c3-6.3 4.8-9.4 10.2-11.7 15.7-7 24.5 8.4 22.8 21.7 38.8-11.2 14-72-22-54-4.3 2.3-8.2 6-11 10-3 4.5-5.2 9.5-5.7 15-.6 7 2.6 13 5.7 19z"/><path fill="#989801" d="M360 87l1 1-1-1m-205 1l1 1-1-1z"/><path fill="#6c6c02" d="M178 88l1 1-1-1z"/><path fill="#444b19" d="M276 88l1 1z"/><path fill="#576e57" d="M297 88l1 1-1-1z"/><path fill="#444b19" d="M235 89l1 1-1-1z"/><path fill="#6c6c02" d="M275 89l1 1-1-1z"/><path fill="#779375" d="M319 89l1 1-1-1z"/><path fill="#9fc79f" d="M201 90l1 1-1-1z"/><path fill="#FFF" d="M196 147c-2-8-3-15 3.4-21.8 10.5-11 28.7-3.6 30.6 10.8 9.5-9.4 6-29.2-3.2-37.7-12.5-11.8-32-10.4-43 2.7-3.6 4.5-5.2 9.2-6 15-.8 5.6.2 12 2.5 17 3.2 6.8 9 11.2 15.7 14z"/><path fill="#989801" d="M274 90l1 1-1-1z"/><path fill="#576e57" d="M320 90l1 1-1-1z"/><path fill="#989801" d="M175 91l1 1z"/><path fill="#444b19" d="M197 91l1 1-1-1z"/><path fill="#576e57" d="M215 91l1 1-1-1z"/><path fill="#989801" d="M273 91l1 1-1-1z"/><path fill="#9fc79f" d="M291 91l1 1-1-1zm-1 1l1 1-1-1z"/><path fill="#444b19" d="M324 92l1 1-1-1z"/><path fill="#6c6c02" d="M238 93l1 1-1-1m34 0l1 1-1-1z"/><path fill="#576e57" d="M288 93l1 1-1-1z"/><path fill="#95bb95" d="M289 93l1 1-1-1z"/><path fill="#444b19" d="M325 93l1 1-1-1z"/><path fill="#6c6c02" d="M173 94l1 1-1-1z"/><path fill="#9fc79f" d="M191 94l1 1-1-1z"/><path fill="#779375" d="M221 94l1 1-1-1z"/><path fill="#444b19" d="M222 94l1 1-1-1"/><path fill="#6c6c02" d="M271 94l1 1-1-1z"/><path fill="#95bb95" d="M287 94l1 1-1-1z"/><path fill="#779375" d="M326 94l1 1-1-1z"/><path fill="#444b19" d="M240 95l1 1-1-1z"/><path fill="#9fc79f" d="M286 95l1 1-1-1z"/><path fill="#989801" d="M364 95l1 1z"/><path fill="#9fc79f" d="M285 96l1 1-1-1zm-1 1l1 1-1-1z"/><path fill="#576e57" d="M126 99l1 1-1-1z"/><path fill="#9fc79f" d="M185 99l1 1-1-1z"/><path fill="#444b19" d="M366 99l1 1-1-1z"/><path fill="#6c6c02" d="M169 100l1 1-1-1z"/><path fill="#9fc79f" d="M184 100l1 1-1-1z"/><path fill="#989801" d="M345 100l1 1-1-1m21 0l1 1-1-1z"/><path fill="#6c6c02" d="M149 101l1 1-1-1z"/><path fill="#9fc79f" d="M182 102l1 1-1-1z"/><path fill="#444b19" d="M230 102l1 1-1-1z"/><path fill="#6c6c02" d="M244 102l1 1-1-1z"/><path fill="#444b19" d="M346 103l1 1-1-1zm-113 4l1 1-1-1z"/><path fill="#989801" d="M166 108l1 1-1-1z"/><path fill="#6c6c02" d="M246 108l1 1-1-1z"/><path fill="#576e57" d="M278 108l1 1-1-1"/><path fill="#444b19" d="M369 108l1 1-1-1m-223 1l1 1-1-1z"/><path fill="#576e57" d="M178 109l1 1-1-1z"/><path fill="#95bb95" d="M234 110l1 1-1-1z"/><path fill="#444b19" d="M372 117l1 1-1-1z"/><path fill="#576e57" d="M213 119l1 1-1-1z"/><path fill="#779375" d="M206 120l1 1-1-1"/><path fill="#9fc79f" d="M295 121l1 1-1-1z"/><path fill="#576e57" d="M304 121l1 1-1-1z"/><path fill="#444b19" d="M221 122l1 1-1-1z"/><path fill="#6c6c02" d="M143 123l1 1-1-1z"/><path fill="#9fc79f" d="M200 123l1 1-1-1z"/><path fill="#576e57" d="M223 123l1 1-1-1z"/><path fill="#9fc79f" d="M199 124l1 1-1-1z"/><path fill="#576e57" d="M224 124l1 1-1-1m54 0l1 1-1-1z"/><path fill="#9fc79f" d="M289 124l1 1-1-1z"/><path fill="#926a61" d="M310 124l1 1-1-1z"/><path fill="#95bb95" d="M311 124l1 1-1-1z"/><path fill="#9fc79f" d="M198 125l1 1-1-1z"/><path fill="#444b19" d="M225 125l1 1-1-1z"/><path fill="#989801" d="M247 125l1 1-1-1z"/><path fill="#6c6c02" d="M266 125l1 1-1-1z"/><path fill="#9fc79f" d="M312 125l1 1-1-1z"/><path fill="#444b19" d="M166 130l1 1-1-1z"/><path fill="#9fc79f" d="M195 130l1 1-1-1z"/><path fill="#95bb95" d="M315 130l1 1-1-1z"/><path fill="#926a61" d="M179 131l1 1-1-1z"/><path fill="#9fc79f" d="M229 131l1 1-1-1z"/><path fill="#779375" d="M233 131l1 1-1-1z"/><path fill="#576e57" d="M281 131l1 1-1-1z"/><path fill="#95bb95" d="M284 131l1 1-1-1z"/><path fill="#9fc79f" d="M282 132l1 1-1-1z"/><path fill="#989801" d="M345 132l1 1-1-1m-178 1l1 1-1-1z"/><path fill="#444b19" d="M180 133l1 1-1-1z"/><path fill="#576e57" d="M194 133l1 1-1-1z"/><path fill="#444b19" d="M232 133l1 1-1-1zm-1 2l1 1-1-1"/><path fill="#6c6c02" d="M169 138l1 1-1-1z"/><path fill="#444b19" d="M243 138l1 1-1-1m28 0l1 1-1-1z"/><path fill="#6c6c02" d="M342 138l1 1-1-1z"/><path fill="#9fc79f" d="M169 139l1 1-1-1z"/><path fill="#989801" d="M243 139l1 1-1-1z"/><path fill="#6c6c02" d="M272 139l1 1-1-1z"/><path fill="#444b19" d="M325 139l1 1-1-1z"/><path fill="#9fc79f" d="M342 139l1 1-1-1m-100 1l1 1-1-1z"/><path fill="#989801" d="M273 141l1 1-1-1z"/><path fill="#6c6c02" d="M274 142l1 1-1-1zm1 1l1 1-1-1z"/><path fill="#444b19" d="M190 144l1 1-1-1z"/><path fill="#6c6c02" d="M276 144l1 1-1-1z"/><path fill="#576e57" d="M192 145l1 1-1-1z"/><path fill="#444b19" d="M237 146l1 1-1-1z"/><path fill="#6c6c02" d="M175 147l1 1z"/><path fill="#989801" d="M237 147l1 1-1-1m42 0l1 1-1-1z"/><path fill="#6c6c02" d="M176 148l1 1-1-1z"/><path fill="#444b19" d="M236 148l1 1-1-1z"/><path fill="#989801" d="M280 148l1 1-1-1z"/><path fill="#444b19" d="M177 149l1 1-1-1z"/><path fill="#989801" d="M281 149l1 1-1-1z"/><path fill="#444b19" d="M178 150l1 1-1-1z"/><path fill="#6c6c02" d="M282 150l1 1-1-1z"/><path fill="#989801" d="M328 152l1 1-1-1z"/><path fill="#444b19" d="M182 153l1 1-1-1z"/><path fill="#6c6c02" d="M325 153l1 1-1-1z"/><path fill="#989801" d="M326 153l1 1-1-1"/><path fill="#444b19" d="M184 154l1 1-1-1z"/><path fill="#989801" d="M229 154l1 1-1-1"/><path fill="#6c6c02" d="M320 155l1 1-1-1z"/><path fill="#989801" d="M223 157l1 1-1-1m-32 1l1 1z"/><path fill="#9fc79f" d="M215 160l1 1-1-1z"/><path fill="#6c6c02" d="M143 162l1 1zm226 15l1 1-1-1z"/><path fill="#989801" d="M367 183l1 1z"/><path fill="#6c6c02" d="M149 184l1 1-1-1z"/><path fill="#989801" d="M366 185l1 1-1-1z"/><path fill="#444b19" d="M366 186l1 1-1-1z"/><path fill="#576e57" d="M390 187l1 1-1-1z"/><path fill="#989801" d="M364 190l1 1-1-1m-212 1l1 1-1-1z"/><path fill="#444b19" d="M364 191l1 1-1-1z"/><path fill="#989801" d="M153 193l1 1-1-1m207 5l1 1z"/><path fill="#6c6c02" d="M156 199l1 1-1-1z"/><path fill="#444b19" d="M157 200l1 1-1-1z"/><path fill="#6c6c02" d="M359 200l1 1-1-1z"/><path fill="#444b19" d="M359 201l1 1-1-1z"/><path fill="#989801" d="M161 207l1 1-1-1z"/><path fill="#444b19" d="M355 207l1 1-1-1z"/><path fill="#989801" d="M162.3 208.7c-108.2 202.2-54 101 0 0z"/><path fill="#444b19" d="M354 208l1 1-1-1z"/><path fill="#989801" d="M163 210l1 1-1-1m3 4l1 1-1-1m1 1l1 1-1-1m182 0l1 1-1-1m-181 1l1 1-1-1zm2 3l1 1-1-1m1 1l1 1-1-1m1 1l1 1-1-1m172 0l1 1-1-1z"/><path fill="#6c6c02" d="M173 222l1 1-1-1z"/><path fill="#989801" d="M343 222l1 1-1-1m-1 1l1 1-1-1z"/><path fill="#6c6c02" d="M175 224l1 1-1-1z"/><path fill="#989801" d="M341 224l1 1-1-1z"/><path fill="#6c6c02" d="M176 225l1 1-1-1z"/><path fill="#989801" d="M177 226l1 1-1-1m1 1l1 1-1-1m1 1l1 1-1-1m158 0l1 1-1-1z"/><path fill="#6c6c02" d="M180 229l1 1-1-1z"/><path fill="#989801" d="M336 229l1 1-1-1z"/><path fill="#6c6c02" d="M181 230l1 1-1-1z"/><path fill="#989801" d="M334 230l1 1-1-1z"/><path fill="#444b19" d="M335 230l1 1-1-1z"/><path fill="#6c6c02" d="M182 231l1 1-1-1z"/><path fill="#989801" d="M184 232l1 1-1-1m1 1l1 1-1-1m1 1l1 1-1-1m1 1l1 1-1-1m3 2l1 1z"/><path fill="#6c6c02" d="M326 237l1 1-1-1z"/><path fill="#444b19" d="M191 238l1 1-1-1z"/><path fill="#989801" d="M192 238l1 1-1-1z"/><path fill="#6c6c02" d="M320 241l1 1-1-1m-1 1l1 1-1-1z"/><path fill="#5a081a" d="M334 248c-222.7 176-111.3 88 0 0z"/><path fill="#ff013d" d="M237 270c-8.5 16-15.4 36.6-20 54-1.4 5-6.3 11.7-5 17 3.2 12.8 17.2 22.5 29 25.6 22.3 5.8 48.2 7.5 70-1 4-1.6 9.7-3 12-6.8 2-3.6.8-9 1.2-12.8 1-10.2 6.5-18.5 16.8-21 0-9 1.7-18.7 8.5-25.5 4-4 10.2-6 14-1.2 9.8 12.3-3.6 27.7 12.5 36.7 13.8-29.4 26.3-75-15-88.7-5.2-1.8-11.4-4.7-17-4-10.3 1-22.7 14-32.4 18.6-22.3 10-50.6 14-74.6 9z"/><path fill="#444b19" d="M199 243l1 1-1-1z"/><path fill="#989801" d="M315 244l1 1z"/><path fill="#444b19" d="M202 245l1 1-1-1z"/><path fill="#989801" d="M313 245l1 1-1-1z"/><path fill="#6c6c02" d="M314 245l1 1-1-1z"/><path fill="#989801" d="M311 246l1 1-1-1z"/><path fill="#444b19" d="M205 247l1 1-1-1z"/><path fill="#5a081a" d="M365 247l1 1-1-1z"/><path fill="#6c6c02" d="M206.7 249.3c-137.8 175-69 87.6 0 0z"/><path fill="#576e57" d="M401 250l1 1-1-1z"/><path fill="#5a081a" d="M181 251c-120.7 174-60.3 87 0 0zm147 0l1 1-1-1z"/><path fill="#a30127" d="M181 253c-120.7 172.7-60.3 86.3 0 0z"/><path fill="#ff013d" d="M137 338c3.2-4.2 6.6-7.2 11-10-2-4-6.7-6.5-7.5-11-1.6-9.4 12.2-21 20.5-21.8 4.4-.5 12.3 5 15.5 3 2.5-1.7 3.4-6.6 4.5-9.2 3-7.5 13-26 8-33.7-2.5-4-7.7-3.8-13.4-2.2-7.6 3-16 8-19.6 10-13.6 8-26.5 17-39 27-4 3-12.3 8-13.5 13-1.2 6 3 11 6.2 14 7.7 9 17.2 17 27.3 22z"/><path fill="#a30127" d="M185.7 252.3c-123.8 173-62 86.6 0 0zM170 255l1 1-1-1z"/><path fill="#989801" d="M289 255l1 1-1-1z"/><path fill="#a30127" d="M320 256l1 1-1-1z"/><path fill="#444b19" d="M230 257l1 1-1-1z"/><path fill="#989801" d="M281 257l1 1-1-1z"/><path fill="#5a081a" d="M318 257l1 1-1-1zm-154.7 1.7c-109 169-54.4 84.4 0 0z"/><path fill="#6c6c02" d="M205 258l1 1-1-1m23 0l1 1-1-1z"/><path fill="#5a081a" d="M161 259l1 1z"/><path fill="#6c6c02" d="M227 259l1 1-1-1z"/><path fill="#a30127" d="M160.3 260.7c-107 167.5-53.4 83.7 0 0z"/><path fill="#444b19" d="M226 260l1 1-1-1z"/><path fill="#a30127" d="M154 263l1 1-1-1"/><path fill="#989801" d="M223 267l1 1-1-1z"/><path fill="#5a081a" d="M386 267l1 1-1-1m-198 1l1 1-1-1z"/><path fill="#989801" d="M201 268l1 1-1-1z"/><path fill="#444b19" d="M223.3 268.7c-149 162.2-74.4 81 0 0z"/><path fill="#6c6c02" d="M222 270c-148 161.3-74 80.7 0 0z"/><path fill="#5a081a" d="M236 270l1 1-1-1z"/><path fill="#989801" d="M200 271c-133.3 160.7-66.7 80.3 0 0z"/><path fill="#5a081a" d="M245 271l1 1-1-1"/><path fill="#a30127" d="M139 273l1 1-1-1z"/><path fill="#5a081a" d="M186.3 273.7c-124.2 159-62 79.4 0 0z"/><path fill="#a30127" d="M137 274l1 1-1-1z"/><path fill="#6c6c02" d="M199 274l1 1-1-1z"/><path fill="#a30127" d="M185 275l1 1z"/><path fill="#444b19" d="M198.5 276.6c-132.3 157-66.2 78.5 0 0z"/><path fill="#a30127" d="M234 275l1 1-1-1zm-100 1l1 1-1-1zm-1 1l1 1-1-1z"/><path fill="#5a081a" d="M131 278l1 1-1-1m53.3.7s0 1 .4.6c0 0 0-1-.4-.6zm-54.3.3l1 1-1-1z"/><path fill="#a30127" d="M183.3 280.7c-122.2 154.2-61 77 0 0z"/><path fill="#5a081a" d="M127 281l1 1-1-1z"/><path fill="#a30127" d="M125.7 282.3c-.5.5.6.4.6.4.5-.5-.6-.4-.6-.4z"/><path fill="#6c6c02" d="M218 282l1 1-1-1z"/><path fill="#5a081a" d="M231 282l1 1-1-1m-107 1l1 1-1-1z"/><path fill="#9fc79f" d="M83 286l1 1-1-1z"/><path fill="#444b19" d="M216.3 287.7c-144.2 149.5-72 74.7 0 0z"/><path fill="#a30127" d="M180 289l1 1-1-1z"/><path fill="#989801" d="M215 289c-143.3 148.7-71.7 74.3 0 0z"/><path fill="#6c6c02" d="M193 290l1 1-1-1m22.3.7s0 1 .4.6c0 0 0-1-.4-.6z"/><path fill="#989801" d="M192.3 293.7c-128.2 145.5-64 72.7 0 0z"/><path fill="#a30127" d="M109 295l1 1-1-1m57 0c-110.7 144.7-55.3 72.3 0 0z"/><path fill="#5a081a" d="M178.3 295.7c-119 144.2-59.4 72 0 0z"/><path fill="#6c6c02" d="M191.3 295.7c-127.5 144.2-63.7 72 0 0z"/><path fill="#989801" d="M213 295l1 1-1-1z"/><path fill="#a30127" d="M108 296l1 1-1-1m46.7.3c-103 143.8-51.6 72 0 0z"/><path fill="#6c6c02" d="M213 296l1 1-1-1z"/><path fill="#a30127" d="M107 297l1 1z"/><path fill="#5a081a" d="M171.3 297.7c-114.2 143-57 71.4 0 0z"/><path fill="#989801" d="M191 297l1 1-1-1z"/><path fill="#a30127" d="M226 297l1 1-1-1m-119.7 1.7c-71 142.2-35.4 71 0 0z"/><path fill="#5a081a" d="M177 298l1 1-1-1z"/><path fill="#444b19" d="M190.3 298.7c-127 142.2-63.4 71 0 0z"/><path fill="#5a081a" d="M173.7 299.3c-115.8 141.8-58 71 0 0z"/><path fill="#a30127" d="M176 299l1 1-1-1m-74 3c-68 140-34 70 0 0z"/><path fill="#5a081a" d="M147 302c1 1 1 1 0 0"/><path fill="#6c6c02" d="M188 304l1 1-1-1z"/><path fill="#989801" d="M210 304l1 1-1-1z"/><path fill="#6c6c02" d="M209 306l1 1-1-1z"/><path fill="#444b19" d="M208 312c-138.7 133.3-69.3 66.7 0 0z"/><path fill="#a30127" d="M106 312l1 1z"/><path fill="#444b19" d="M185 312l1 1-1-1z"/><path fill="#6c6c02" d="M207.3 312.7c-138.2 133-69 66.4 0 0z"/><path fill="#a30127" d="M107 313l1 1-1-1z"/><path fill="#5a081a" d="M140 313c-93.3 132.7-46.7 66.3 0 0z"/><path fill="#989801" d="M185 313l1 1-1-1z"/><path fill="#a30127" d="M108 314l1 1-1-1z"/><path fill="#989801" d="M206.3 314.7c-137.5 131.5-68.7 65.7 0 0z"/><path fill="#a30127" d="M109 315l1 1-1-1m1.3 1.7c-73.5 130.2-36.7 65 0 0z"/><path fill="#444b19" d="M183.3 317.7c-122.2 129.5-61 64.7 0 0z"/><path fill="#a30127" d="M111 318l1 1-1-1m1 1l1 1-1-1z"/><path fill="#989801" d="M183 319l1 1-1-1z"/><path fill="#5a081a" d="M218 319l1 1-1-1z"/><path fill="#a30127" d="M382 320l1 1-1-1m-267 1l1 1-1-1m1 1l1 1zm1 1l1 1-1-1m1 1l1 1-1-1m28 1l1 1-1-1m2.3 1.7c-99 123.5-49.4 61.7 0 0z"/><path fill="#444b19" d="M180 326l1 1-1-1z"/><path fill="#a30127" d="M335 326l1 1-1-1m-213 1l1 1-1-1m93.3.7c-143.5 123-71.7 61.4 0 0zm-92.3.3l1 1-1-1z"/><path fill="#5a081a" d="M147 328l1 1-1-1z"/><path fill="#6c6c02" d="M201 328l1 1-1-1z"/><path fill="#444b19" d="M201.3 329.7c-134.2 121.5-67 60.7 0 0z"/><path fill="#5a081a" d="M214.3 329.7c-143 121.5-71.4 60.7 0 0z"/><path fill="#989801" d="M184 330l1 1-1-1z"/><path fill="#5a081a" d="M373 333l1 1z"/><path fill="#444b19" d="M191 334l1 1-1-1z"/><path fill="#989801" d="M192 334l1 1-1-1z"/><path fill="#6c6c02" d="M199 334l1 1-1-1z"/><path fill="#a30127" d="M327 334l1 1-1-1z"/><path fill="#5a081a" d="M374 334l1 1-1-1z"/><path fill="#6c6c02" d="M194.3 335.7c-129.5 117.5-64.7 58.7 0 0z"/><path fill="#5a081a" d="M375 335l1 1-1-1m-241.3 1.3c-89 117-44.6 58.6 0 0z"/><path fill="#444b19" d="M195 336l1 1-1-1z"/><path fill="#989801" d="M198.3 336.7c-132.2 117-66 58.4 0 0z"/><path fill="#9fc79f" d="M104 337l1 1-1-1z"/><path fill="#444b19" d="M197 337l1 1-1-1z"/><path fill="#926a61" d="M343 338l1 1-1-1z"/><path fill="#fbb5a7" d="M344 338.7c-8.5 3.5-8.5 27.2-1.4 32.3 3.8 2.8 17.2-.7 21.4-2.3 23.5-9.5-4.6-36-20-30z"/><path fill="#444b19" d="M341 339l1 1-1-1m-1 1l1 1-1-1z"/><path fill="#926a61" d="M357 340l1 1-1-1z"/><path fill="#a30127" d="M211 341l1 1-1-1zm1 1l1 1-1-1z"/><path fill="#926a61" d="M339 342l1 1-1-1m22 0l1 1-1-1z"/><path fill="#5a081a" d="M324 343l1 1-1-1z"/><path fill="#b9867c" d="M159 344c-106 112-53 56 0 0z"/><path fill="#a30127" d="M213 344l1 1-1-1z"/><path fill="#444b19" d="M364 344l1 1-1-1z"/><path fill="#fbb5a7" d="M201 365c-6.8-9-22-17.4-33-19.5-4.5-1-12.2-1.7-14.2 3.6-2.6 7 4 13 9.2 16 11.8 6 26 4 38 1z"/><path fill="#926a61" d="M168 345l1 1-1-1z"/><path fill="#5a081a" d="M214.3 345.7c-143 111-71.4 55.4 0 0z"/><path fill="#444b19" d="M365 345l1 1-1-1z"/><path fill="#a30127" d="M215 347l1 1-1-1z"/><path fill="#b9867c" d="M176 348l1 1-1-1z"/><path fill="#444b19" d="M178.7 349.3c-119 108.5-59.6 54.3 0 0z"/><path fill="#5a081a" d="M216 349l1 1-1-1z"/><path fill="#926a61" d="M180.7 350.3c-120.5 107.8-60.3 54 0 0z"/><path fill="#5a081a" d="M217 350l1 1-1-1z"/><path fill="#444b19" d="M370 350l1 1-1-1z"/><path fill="#b9867c" d="M182 351l1 1-1-1z"/><path fill="#576e57" d="M183 351l1 1-1-1z"/><path fill="#5a081a" d="M218 351l1 1-1-1z"/><path fill="#576e57" d="M371 351l1 1-1-1z"/><path fill="#444b19" d="M185 352l1 1-1-1zm-32 2c-102 105.3-51 52.7 0 0z"/><path fill="#5a081a" d="M222 356l1 1-1-1"/><path fill="#926a61" d="M192 357l1 1-1-1z"/><path fill="#5a081a" d="M223 357l1 1-1-1z"/><path fill="#b9867c" d="M155 358l1 1-1-1z"/><path fill="#444b19" d="M338 358l1 1-1-1z"/><path fill="#b9867c" d="M194.7 359.3c-129.8 101.8-65 51 0 0z"/><path fill="#5a081a" d="M320 361l1 1-1-1z"/><path fill="#926a61" d="M199 362l1 1-1-1z"/><path fill="#a30127" d="M319 362l1 1-1-1z"/><path fill="#926a61" d="M200 363l1 1-1-1z"/><path fill="#5a081a" d="M229 363l1 1-1-1z"/><path fill="#444b19" d="M161 364l1 1-1-1z"/><path fill="#926a61" d="M162 364l1 1-1-1m39 0l-1 2h2c0-2 .3-1.4-1-2z"/><path fill="#444b19" d="M163.7 365.3c-109 97.8-54.6 49 0 0z"/><path fill="#b9867c" d="M199 365l1 1-1-1z"/><path fill="#5a081a" d="M311.7 365.3c-207.8 97.8-104 49 0 0z"/><path fill="#444b19" d="M196 366c-130.7 97.3-65.3 48.7 0 0z"/><path fill="#576e57" d="M134 367l1 1-1-1z"/><path fill="#b9867c" d="M191 367l1 1-1-1z"/><path fill="#a30127" d="M244 367l1 1-1-1m61 0l1 1-1-1z"/><path fill="#444b19" d="M366 367l1 1-1-1z"/><path fill="#926a61" d="M364 368l1 1-1-1z"/><path fill="#5a081a" d="M298 369l1 1-1-1z"/><path fill="#95bb95" d="M357 385c-17 3.2-27 3-31-16-12 1.6-23.6 7.8-36 9.6-4 .6-11-1-14.3 2-5 4.8-5.4 14-5.7 20.4-.3 4.7-2.4 9.3-1.6 14 1.7 10.3 13.3 21.2 19.7 29 5 5.7 11 14.3 18 16.5 9 2.4 17-4.7 23-8.8 10-6 20-13.5 25-23.7 3-4.8 1-11.8 1-17 1-8.7 2-17.3 2-26z"/><path fill="#444b19" d="M358 370l1 1-1-1z"/><path fill="#576e57" d="M305 375l1 1-1-1z"/><path fill="#9fc79f" d="M157 384c0 14-.5 26 5.2 39 4.8 11 11 21 19 30 7 7.7 14.8 9 24.8 9 17.2 0 33.7-6.2 42.5-22 11-20 3.6-55-21.5-61.3-8-2-23.7 6.3-32 8-13.7 3-24.8-.4-38-2.7z"/><path fill="#779375" d="M290 378l1 1-1-1zm-61 1l1 1-1-1z"/><path fill="#576e57" d="M216 380c-144 88-72 44 0 0z"/><path fill="#444b19" d="M274.7 381.3c-183 87-91.6 43.6 0 0z"/><path fill="#779375" d="M232.3 381.7c-155 87-77.4 43.4 0 0z"/><path fill="#95bb95" d="M211 382c-140.7 86.7-70.3 43.3 0 0z"/><path fill="#444b19" d="M273 385c-182 84.7-91 42.3 0 0z"/><path fill="#779375" d="M208 383l1 1-1-1m27 0l1 1-1-1m1 1l1 1-1-1"/><path fill="#576e57" d="M162 385l1 1-1-1z"/><path fill="#444b19" d="M238 385l1 1-1-1zm1 1l1 1-1-1m33 0l1 1-1-1"/><path fill="#9fc79f" d="M137 387l1 1-1-1z"/><path fill="#779375" d="M191 387l1 1-1-1z"/><path fill="#576e57" d="M240 387l1 1-1-1m1 1l1 1-1-1m1 1l1 1-1-1m-106 1l1 1-1-1m107 0l1 1-1-1z"/><path fill="#9fc79f" d="M136 391l1 1-1-1z"/><path fill="#576e57" d="M244 391l1 1-1-1m2 3l1 1z"/><path fill="#779375" d="M155 395l1 1-1-1z"/><path fill="#444b19" d="M251 403l1 1-1-1z"/><path fill="#576e57" d="M365 406c-1 6.8 1.4 18.2-1.5 24.2-7.2 15.3-24.4 25.4-38.5 33.7-6.6 3-13.2 7-21 5-16.8-6-21-26-35-34 0 9 .3 16 3.7 25 17.6 42 71.5 11 89.5-14 7.4-11 13.3-30 2.8-40m-208 3l1 1-1-1z"/><path fill="#779375" d="M268 416l1 1-1-1zm-15 9l1 1z"/><path fill="#444b19" d="M274 426l1 1z"/><path fill="#779375" d="M275 427l1 1-1-1zm3 4l1 1-1-1z"/><path fill="#576e57" d="M155 432c2.6 11 11 20.8 17.3 30 2 3.3 4.5 8.6 8 10.4 4.6 2.4 11.7 2.2 16.7 2.8 15.3 1.8 33.3 2.6 48-2.5 5.6-2 11.8-4 14.7-9.7 3-5.7-.6-14-1.7-20-5.6 6-9.4 12.4-16 17.5-14.2 11-43 17.8-59 6-12.4-9-17.4-24.2-28-34.5m11 0l1 1-1-1m113 0l1 1-1-1z"/><path fill="#444b19" d="M362 432l1 1-1-1z"/><path fill="#576e57" d="M167 433l1 1-1-1zm182 0l1 1-1-1z"/><path fill="#444b19" d="M280 434l1 1-1-1m1 1l1 1-1-1z"/><path fill="#576e57" d="M282 436l1 1-1-1z"/><path fill="#444b19" d="M169 437l1 1-1-1zm188 0l1 1-1-1z"/><path fill="#779375" d="M170 438l1 1-1-1"/><path fill="#444b19" d="M356 438l1 1-1-1m-186 1l1 1-1-1z"/><path fill="#576e57" d="M284 439l1 1-1-1z"/><path fill="#444b19" d="M355 439l1 1-1-1m-197 1l1 1-1-1m13 0l1 1-1-1z"/><path fill="#576e57" d="M248 440l1 1-1-1z"/><path fill="#444b19" d="M285 440l1 1-1-1z"/><path fill="#779375" d="M343 440l1 1-1-1z"/><path fill="#444b19" d="M354 440l1 1-1-1m-11 1l1 1-1-1m-54 4l1 1-1-1m-30 7c-172.7 40-86.3 20 0 0z"/><path fill="#576e57" d="M290 446l1 1-1-1zm-54 7l1 1-1-1m90 0l1 1-1-1z"/><path fill="#444b19" d="M341 453l1 1-1-1zm-44 1l1 1-1-1zm58 0l1 1-1-1z"/><path fill="#576e57" d="M233 455l1 1-1-1z"/><path fill="#444b19" d="M248 455l1 1-1-1m38 0l1 1-1-1m12 0l1 1-1-1z"/><path fill="#576e57" d="M320 456l1 1-1-1z"/><path fill="#444b19" d="M321 456l1 1-1-1z"/><path fill="#779375" d="M229 457l1 1-1-1z"/><path fill="#444b19" d="M288 457l1 1-1-1z"/><path fill="#779375" d="M319 457l1 1-1-1z"/><path fill="#444b19" d="M289 458l1 1z"/><path fill="#9fc79f" d="M375 459l1 1-1-1z"/><path fill="#576e57" d="M200 462c-133.3 33.3-66.7 16.7 0 0z"/></svg> \ No newline at end of file
diff --git a/static/image/lambda.svg b/static/image/lambda.svg
new file mode 100644
index 0000000..76c26c2
--- /dev/null
+++ b/static/image/lambda.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg5" width="61.538mm" height="61.538mm" version="1.1" viewBox="0 0 61.538 61.538" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="layer1" transform="translate(-78.308 -101.72)"><path id="path209" d="m121.81 162.75c-0.59549-0.32382-1.0577-1.017-1.3433-2.0144-2.8775-10.051-10.168-30.731-10.714-30.393-0.82553 0.51021-8.7304 17.972-12.047 26.611l-1.2984 3.3824-3.9854 1.1155c-2.192 0.61353-4.3197 1.1155-4.7282 1.1155-0.89868 0-1.8411-1.2469-1.8411-2.4359 0-1.0914 5.0335-10.334 13.717-25.187l6.7334-11.517-1.9168-4.4447c-2.3542-5.4589-3.3324-7.1567-4.9327-8.5618-1.8477-1.6223-3.5422-2.0198-6.5166-1.5286-3.4653 0.57228-3.6407 0.54019-4.5271-0.82819-1.3395-2.068-1.132-2.4558 1.9403-3.6259 10.235-3.8979 16.176-3.6193 20.307 0.95239 1.8593 2.058 3.485 5.524 8.4601 18.037 4.9127 12.356 8.4541 20.548 12.641 29.242 2.2349 4.6405 2.7038 5.8845 2.3448 6.2208-0.44265 0.41458-10.564 4.3963-11.106 4.3689-0.15347-8e-3 -0.6876-0.23629-1.187-0.50783z"/></g></svg>
diff --git a/static/image/lobsters.png b/static/image/lobsters.png
new file mode 100644
index 0000000..e75cc99
--- /dev/null
+++ b/static/image/lobsters.png
Binary files differ
diff --git a/static/image/oneko.gif b/static/image/oneko.gif
new file mode 100644
index 0000000..a009c2c
--- /dev/null
+++ b/static/image/oneko.gif
Binary files differ
diff --git a/static/image/opengraph-icon-coq.jpg b/static/image/opengraph-icon-coq.jpg
new file mode 100644
index 0000000..33c969a
--- /dev/null
+++ b/static/image/opengraph-icon-coq.jpg
Binary files differ
diff --git a/static/image/portrait.png b/static/image/portrait.png
new file mode 100644
index 0000000..dfaee55
--- /dev/null
+++ b/static/image/portrait.png
Binary files differ
diff --git a/static/image/profile-picture.jpg b/static/image/profile-picture.jpg
new file mode 100644
index 0000000..cd2e07e
--- /dev/null
+++ b/static/image/profile-picture.jpg
Binary files differ
diff --git a/static/image/tango-emblem-important.png b/static/image/tango-emblem-important.png
new file mode 100644
index 0000000..81c455b
--- /dev/null
+++ b/static/image/tango-emblem-important.png
Binary files differ
diff --git a/static/image/tor.svg b/static/image/tor.svg
new file mode 100644
index 0000000..ff0f894
--- /dev/null
+++ b/static/image/tor.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="500" height="500" viewBox="0 0 500 500" xml:space="preserve">
+<defs>
+</defs>
+<rect x="0" y="0" width="100%" height="100%" fill="transparent"></rect>
+<g transform="matrix(0 0 0 0 0 0)" id="4fc577b5-c66b-46cc-95ea-aa650b6bee37" >
+</g>
+<g transform="matrix(1 0 0 1 250 250)" id="1e5c576a-bbc5-453b-bf9f-d6e354a4a78e" >
+<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1; visibility: hidden;" vector-effect="non-scaling-stroke" x="-250" y="-250" rx="0" ry="0" width="500" height="500" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 302.85 86.5)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(171,205,3); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-275.75, -93.3)" d="M 264.513 77.977773 L 259.596 97.50677400000001 C 266.561 83.713773 277.623 73.33477300000001 290.325 64.183773 C 281.038 74.972773 272.57099999999997 85.76277400000001 267.381 96.551774 C 276.12199999999996 84.259773 287.86699999999996 77.43177299999999 301.114 72.92477299999999 C 283.496 88.63077399999999 269.51171999999997 105.48405 258.85871999999995 122.41904999999998 L 250.39171999999994 118.73204999999999 C 251.89271999999994 105.21104999999999 257.0009999999999 91.36277399999999 264.5129999999999 77.97777299999998 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 202.88 314.38)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,252,219); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-237.32, -181.37)" d="M 241.90113 115.14152 L 258.01713 121.82746 C 258.01713 125.92546 257.684 138.42449 260.24651 142.11149 C 287.0494 176.63059 282.54 245.82478 254.817 247.59978 C 212.60044 247.59978 196.5 218.92078 196.5 192.56177000000002 C 196.5 168.52477000000002 225.316 152.54577000000003 242.525 138.34277000000003 C 246.895 134.51877000000002 246.13613 126.06752000000003 241.90113 115.14152000000003 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 329.25 322.02)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(125,70,152); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-285.93, -184.3)" d="M 258.02197 121.58695 L 263.83 124.54977 C 263.284 128.37277 264.103 136.84177 267.926 139.02577 C 284.86199999999997 149.54176999999999 300.84 161.01377 307.123 172.48577 C 329.521 212.91178 291.417 250.33178 258.503 246.78078 C 276.394 233.53278 281.584 206.35577 274.892 176.71877 C 272.161 165.10977 267.926 154.59377 260.414 142.71177 C 257.15979 136.87931 258.29597 129.64595 258.02197 121.58695 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 250 311.11)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-255.61, -180.13)" d="M 255.226 120.58877 L 267.244 122.22776999999999 C 263.69300000000004 133.97277 274.21000000000004 142.16676999999999 277.624 144.07977 C 285.27201 148.31377 292.64701 152.68376999999998 298.52001 158.00977 C 309.58301 168.11577 315.86501 182.31977 315.86501 197.34277 C 315.86501 212.22877 309.03601 226.56877 297.56401 236.12877 C 286.77501 245.14277 271.889 248.96677 257.412 248.96677 C 248.39799999999997 248.96677 240.33999999999997 248.55777 231.59999999999997 245.68877 C 211.66099999999997 238.99677 196.77399999999997 221.92577 195.54499999999996 201.43877 C 194.45199999999997 185.45977 198.00299999999996 173.30477000000002 210.43199999999996 160.60377 C 216.84999999999997 153.91177 229.82499999999996 146.26377 238.70299999999997 140.11777 C 243.07399999999998 137.11277 247.71699999999998 128.64477 238.83899999999997 112.66677000000001 L 240.61499999999998 111.30077000000001 L 253.77158999999997 120.11280000000002 L 242.664 115.53477000000002 C 243.61999999999998 116.90077000000002 246.21499999999997 123.04677000000002 246.762 124.82177000000003 C 247.991 129.87477000000004 247.445 134.79277000000002 246.352 136.97677000000002 C 240.75300000000001 147.08377000000002 231.193 149.81477 224.228 155.55077000000003 C 211.936 165.65677000000002 198.55200000000002 173.71477000000004 200.054 201.43877000000003 C 200.737 215.09577000000004 211.39000000000001 231.75777000000002 227.368 239.54277000000002 C 236.382 243.91377000000003 246.762 245.68877 257.278 246.23477000000003 C 266.701 246.64477000000002 284.72901 241.04477000000003 294.56201000000004 232.85077 C 305.07801000000006 224.11077 310.95101000000005 210.86277 310.95101000000005 197.34277 C 310.95101000000005 183.68477000000001 305.48801000000003 170.71077 295.24501000000004 161.55977000000001 C 289.37201000000005 156.23377000000002 279.67600000000004 149.81477 273.66700000000003 146.39977000000002 C 267.658 142.98577000000003 260.146 133.42577000000003 262.60400000000004 124.27577000000002 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 186.45 348.06)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-230.86, -194.27)" d="M 251.539 140.80177 C 250.30999999999997 147.08477 248.944 158.41977 243.481 162.65377 C 241.159 164.29177 238.837 165.93177 236.379 167.56977 C 226.546 174.26277000000002 216.712 180.54377 212.206 196.65977 C 211.25 200.07477 212.07 203.76177 212.89 207.17577 C 215.34799999999998 217.00877 222.313 227.66177 227.77599999999998 233.94477 C 227.77599999999998 234.21777 228.86899999999997 234.90077 228.86899999999997 235.17377000000002 C 233.37599999999998 240.50077000000002 234.74199999999996 242.00277000000003 251.81299999999996 245.82577 L 251.40299999999996 247.73877000000002 C 241.15999999999997 245.00777000000002 232.69299999999996 242.54977000000002 227.36599999999996 236.40277 C 227.36599999999996 236.26677 226.40999999999997 235.30977000000001 226.40999999999997 235.30977000000001 C 220.67399999999998 228.75377 213.70799999999997 217.82877000000002 211.11399999999998 207.58577000000002 C 210.158 203.48777 209.33899999999997 200.34777000000003 210.43099999999998 196.11277 C 215.07399999999998 179.45177 225.18099999999998 172.89577000000003 235.42399999999998 165.93077000000002 C 237.74599999999998 164.42877000000001 240.47699999999998 163.06177000000002 242.66199999999998 161.28677000000002 C 246.89499999999998 158.14677000000003 249.21599999999998 148.58577000000002 251.539 140.80177000000003 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 224.13 380.53)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-245.38, -206.7)" d="M 255.90625 166.74951 C 256.04325 173.85151 255.35 177.41425999999998 257.125 182.46826 C 258.217 185.47225999999998 261.907 189.56975999999997 263 193.53076 C 264.502 198.85775999999998 266.138 204.72976999999997 266 208.28077 C 266 212.37876 265.74375 220.02326 263.96875 228.21826 C 262.61513 234.98934 259.49552 240.79978999999997 254.25 244.09326 C 248.87673 242.98682 242.56776 241.09805 238.84375 237.90576 C 231.60575 231.62375999999998 225.195 221.11926 224.375 211.96826 C 223.693 204.45727 230.64775 193.37975999999998 240.34375 187.78076 C 248.53775 183.00075999999999 250.44375 177.55300999999997 252.21875 168.81201 C 249.76075 176.46000999999998 247.45225 182.87125999999998 239.53125 186.96826 C 228.05925 192.97726 222.17275 203.06452 222.71875 212.62451 C 223.53775 224.91550999999998 228.46025 233.24400999999997 238.15625 239.93701 C 242.25325 242.80500999999998 249.9075 245.83576 254.6875 246.65576 L 254.6875 246.03076 C 258.31243 245.35188 263.00568 239.39809 265.34375 231.34326 C 267.39275 224.10526 268.2005 214.84125999999998 268.0625 208.96827 C 267.9255 205.55427 266.4195 198.16026 263.6875 191.46826 C 262.1855 187.78125999999997 259.878 184.09450999999999 258.375 181.49951 C 256.738 178.90250999999998 256.72625 173.30451 255.90625 166.74951 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 240.35 411.95)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-251.56, -218.72)" d="M 255.09375 193.53076 C 255.22975 198.31076 257.14975 204.43526999999997 257.96875 210.71827 C 258.65275 215.36226 258.35575 220.02651 258.21875 224.12451 C 258.0838 228.86774 256.50355 237.36668999999998 254.34375 241.49951 C 252.30702 240.56547999999998 251.51081 239.50028999999998 250.1875 237.78076 C 248.5495 235.45875999999998 247.43675 233.13675999999998 246.34375 230.40576 C 245.52475 228.35675999999998 244.56725 226.01175999999998 244.15625 223.28076 C 243.61025 219.18375999999998 243.76325 212.77426 248.40625 206.21827 C 251.95725 201.02826 252.771 200.63351 254 194.62451 C 252.36 199.95050999999998 251.1375 200.49351 247.3125 204.99952 C 243.0795 209.91652 242.375 217.15876 242.375 223.03076 C 242.375 225.48976 243.3555 228.21801 244.3125 230.81201 C 245.4045 233.54401 246.34175 236.26400999999998 247.84375 238.31201 C 250.10171 241.63282999999998 252.99173 243.52122999999997 254.40625 243.87451 C 254.41525 243.87651 254.42825 243.87250999999998 254.43745 243.87451 C 254.46775000000002 243.88151 254.50235 243.90000999999998 254.53115000000003 243.90571 L 254.53115000000003 243.74946 C 257.18097 240.79509 258.77559 237.86012 259.3124 234.90571 C 259.9954 231.35471 260.1524 227.79596 260.5624 223.56196 C 260.9714 220.01096 260.67465000000004 215.22796 259.71865 210.31197 C 258.35365 204.16597000000002 256.04965000000004 197.89971 255.09365000000003 193.53071 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 272.78 340.19)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-264.05, -191.27)" d="M 255.499 135.06577 C 255.636 142.16676999999999 256.182 155.41576999999998 258.094 160.60476999999997 C 258.64 162.37976999999998 263.693 170.16476999999998 267.243 179.58776999999998 C 269.702 186.14377 270.248 192.15276999999998 270.658 193.92776999999998 C 272.297 201.71276999999998 270.248 214.82376999999997 267.516 227.25176999999996 C 266.151 233.94376999999997 261.507 242.27476999999996 256.18100000000004 245.55276999999995 L 255.08900000000003 247.46476999999996 C 258.09400000000005 247.32776999999996 265.468 240.08976999999996 268.06300000000005 231.07576999999995 C 272.434 215.77976999999996 274.20900000000006 208.67776999999995 272.16100000000006 191.74276999999995 C 271.88800000000003 190.10276999999996 271.20500000000004 184.50476999999995 268.61000000000007 178.49476999999996 C 264.78600000000006 169.34376999999995 259.3230000000001 160.60376999999997 258.6410000000001 158.82776999999996 C 257.41100000000006 155.96076999999997 255.77200000000008 143.53276999999997 255.49900000000008 135.06576999999996 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 279.5 327.55)" >
+<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-266.65, -186.41)" d="M 258.06151 125.35303 C 257.65636 132.65115 257.548 135.33877 258.913 140.66477 C 260.415 146.53777 268.064 155.00477 271.205 164.70177 C 277.214 183.27577000000002 275.712 207.58577000000002 271.341 226.56877 C 269.70300000000003 233.25977 261.91700000000003 242.95777 254.133 246.09777 L 259.869 247.46377 C 263.01000000000005 247.32677 271.067 239.81577000000001 274.209 231.21177 C 279.261 217.69077 280.218 201.57577 278.169 184.64077 C 278.032 183.00177 275.29999999999995 168.38877 272.70599999999996 162.24277 C 269.018 153.09277 262.462 144.89777 261.78 143.12377 C 260.55199999999996 140.11877 257.85348999999997 133.88015000000001 258.06151 125.35303 z" stroke-linecap="round" />
+</g>
+<g transform="matrix(2.58 0 0 2.58 246.81 319.25)" id="rect2556" >
+<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" x="-0.275" y="-63.01" rx="0" ry="0" width="0.55" height="126.02" />
+</g>
+</svg>
diff --git a/static/image/webmention-logo.png b/static/image/webmention-logo.png
new file mode 100644
index 0000000..a4ffda1
--- /dev/null
+++ b/static/image/webmention-logo.png
Binary files differ
diff --git a/static/js/comment-reaction.js b/static/js/comment-reaction.js
new file mode 100644
index 0000000..1002fa2
--- /dev/null
+++ b/static/js/comment-reaction.js
@@ -0,0 +1,143 @@
+/*
+ * comment-reaction.js -- Simple emoji picker and glue for reaction endpoint
+ * Copyright © 2023 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+function encodeAsFormData(obj) {
+ // Turn the data object into an array of URL-encoded key/value pairs.
+ let urlEncodedDataPairs = [];
+ for (let key in obj) {
+ urlEncodedDataPairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
+ }
+ return urlEncodedDataPairs.join('&');
+}
+
+function postReaction(reaction, callback) {
+ let xhr = new XMLHttpRequest();
+ xhr.open('POST', "/api/comment/react", true);
+
+ // Endpoint expects HTML form data.
+ xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+
+ xhr.onload = function(data) {
+ switch (xhr.status) {
+ case 200:
+ callback();
+ break;
+ case 429:
+ alert("Failed -- you can react at most once an hour.");
+ break;
+ default:
+ let resp = JSON.parse(xhr.response);
+ alert("Failed -- " + resp.error);
+ break;
+ }
+ }
+ xhr.onerror = function(error) {
+ throw new Error(`Request failed: ${error}`);
+ }
+
+ xhr.send(encodeAsFormData(reaction));
+}
+
+function selectorWidgetName(id) {
+ return `emoji-picker-${id}`;
+}
+
+// Simple Emoji picker widget.
+function selectorWidget(id) {
+ function emojiButton(codepoint) {
+ let span = document.createElement("span");
+ span.innerHTML = String.fromCodePoint(codepoint);
+ return span;
+ }
+
+ let selector = document.createElement("div");
+ selector.setAttribute("id", selectorWidgetName(id));
+ selector.setAttribute("class", "emoji-picker");
+
+ for (let i = 0x1F600; i < 0x1F64F; i++) {
+ selector.appendChild(emojiButton(i));
+ }
+ selector.appendChild(emojiButton(0x1F37B));
+ selector.appendChild(emojiButton(0x1F440));
+ selector.appendChild(emojiButton(0x1F446));
+ selector.appendChild(emojiButton(0x1F447));
+ selector.appendChild(emojiButton(0x1F448));
+ selector.appendChild(emojiButton(0x1F449));
+ selector.appendChild(emojiButton(0x1F450));
+ selector.appendChild(emojiButton(0x270A));
+ selector.appendChild(emojiButton(0x270B));
+ selector.appendChild(emojiButton(0x270C));
+ selector.appendChild(emojiButton(0x270D));
+ for (let i = 0x1F90C; i < 0x1F9FF; i++) {
+ selector.appendChild(emojiButton(i));
+ }
+ return selector;
+}
+
+// Add a "react" button to every local comment.
+window.addEventListener("load", () => {
+ // First pass to add a separator for the buttons.
+ let replyButtons = document.querySelectorAll(".comment-reply-button");
+ for (let button of replyButtons) {
+ let parent = button.parentNode;
+ parent.innerHTML += " - ";
+
+ }
+
+ // Second pass to add the actual "react" button.
+ replyButtons = document.querySelectorAll(".comment-reply-button");
+ for (let button of replyButtons) {
+ let id = button.getAttribute("data-reply-to-id");
+ let reactButton = document.createElement("a");
+ reactButton.setAttribute("class", "comment-react-button");
+ reactButton.setAttribute("href", "#");
+ reactButton.setAttribute("data-react-to-id", id);
+ reactButton.innerHTML = "react";
+ button.parentNode.appendChild(reactButton);
+
+ reactButton.addEventListener('click', (e) => {
+ // If we already made a selector widget for this comment, let's "hide" the
+ // one that exists.
+ let existingSelectorWidget = document.getElementById(selectorWidgetName(id));
+ if (existingSelectorWidget !== null) {
+ existingSelectorWidget.remove();
+ e.preventDefault();
+ return;
+ }
+
+ let emojiPicker = selectorWidget(id);
+ button.parentNode.parentNode.insertBefore(emojiPicker, button.parentNode);
+ for (let button of emojiPicker.children) {
+ button.addEventListener('click', (e) => {
+ postReaction({
+ "id": reactButton.getAttribute("data-react-to-id"),
+ "reaction": button.innerHTML
+ }, () => {
+ let reactAcknowledgement = document.createElement("span");
+ reactAcknowledgement.innerHTML = "successfully reacted!"
+ reactButton.replaceWith(reactAcknowledgement);
+ });
+ emojiPicker.remove();
+ });
+ }
+
+ e.preventDefault();
+ });
+ }
+});
diff --git a/static/js/dynamic-comment-form.js b/static/js/dynamic-comment-form.js
new file mode 100644
index 0000000..4be2d52
--- /dev/null
+++ b/static/js/dynamic-comment-form.js
@@ -0,0 +1,94 @@
+/*
+ * dynamic-comment-form.js -- Captcha challenge loader
+ * Copyright © 2022 - 2023 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+function makeRequest (method, url) {
+ return new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+ xhr.onload = function () {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resolve(xhr.response);
+ } else {
+ reject({
+ status: xhr.status,
+ statusText: xhr.statusText
+ });
+ }
+ };
+ xhr.onerror = function () {
+ reject({
+ status: xhr.status,
+ statusText: xhr.statusText
+ });
+ };
+ xhr.send();
+ });
+}
+
+window.addEventListener("load", () => {
+ let primaryDisplay = document.getElementById("comment-form-primary");
+ primaryDisplay.removeAttribute("hidden");
+
+ let altDisplay = document.getElementById("comment-form-alt");
+ altDisplay.remove();
+
+ let trigger = document.getElementById("captcha-challenge-trigger");
+ let triggerBlock = document.getElementById("captcha-trigger-block");
+ trigger.addEventListener("click", (event) => {
+ let captchaField = document.getElementById("comment-captcha");
+
+ let captchaId = document.getElementById("captcha-id");
+ let captchaImage = document.getElementById("captcha-image");
+
+ trigger.innerHTML = "Please wait...";
+ trigger.disabled = true;
+
+ if (captchaId.value === "") {
+ makeRequest("GET", "/api/challenge/captcha")
+ .then(function (data) {
+ // Update form with parsed values.
+ let challengeData = JSON.parse(data);
+ captchaId.value = challengeData["challenge-id"];
+ captchaImage.src = challengeData["image"];
+
+ // Unhide fieldset.
+ captchaField.removeAttribute("hidden");
+
+ // Hide initial trigger.
+ triggerBlock.remove();
+ })
+ .catch(console.err);
+ }
+
+ event.preventDefault();
+ });
+});
+
+
+window.addEventListener("load", () => {
+ let header = document.getElementById("comment-form-header");
+ let replyTo = document.getElementById("reply-to");
+ let buttons = document.querySelectorAll(".comment-reply-button");
+ buttons.forEach((button) => {
+ button.addEventListener("click", (event) => {
+ header.innerHTML = "Comment form (reply mode)";
+ replyTo.value = button.getAttribute("data-reply-to-id");
+ });
+ });
+});
diff --git a/static/js/oneko.js b/static/js/oneko.js
new file mode 100644
index 0000000..b0ba164
--- /dev/null
+++ b/static/js/oneko.js
@@ -0,0 +1,182 @@
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the “Software”), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+(function oneko() {
+ const nekoEl = document.createElement("div");
+ let nekoPosX = 32;
+ let nekoPosY = 32;
+ let mousePosX = 0;
+ let mousePosY = 0;
+ let frameCount = 0;
+ let idleTime = 0;
+ let idleAnimation = null;
+ let idleAnimationFrame = 0;
+ const nekoSpeed = 10;
+ const spriteSets = {
+ idle: [[-3, -3]],
+ alert: [[-7, -3]],
+ scratch: [
+ [-5, 0],
+ [-6, 0],
+ [-7, 0],
+ ],
+ tired: [[-3, -2]],
+ sleeping: [
+ [-2, 0],
+ [-2, -1],
+ ],
+ N: [
+ [-1, -2],
+ [-1, -3],
+ ],
+ NE: [
+ [0, -2],
+ [0, -3],
+ ],
+ E: [
+ [-3, 0],
+ [-3, -1],
+ ],
+ SE: [
+ [-5, -1],
+ [-5, -2],
+ ],
+ S: [
+ [-6, -3],
+ [-7, -2],
+ ],
+ SW: [
+ [-5, -3],
+ [-6, -1],
+ ],
+ W: [
+ [-4, -2],
+ [-4, -3],
+ ],
+ NW: [
+ [-1, 0],
+ [-1, -1],
+ ],
+ };
+ function create() {
+ nekoEl.id = "oneko";
+ nekoEl.style.width = "32px";
+ nekoEl.style.height = "32px";
+ nekoEl.style.position = "fixed";
+ nekoEl.style.backgroundImage = "url('/static/image/oneko.gif')";
+ nekoEl.style.imageRendering = "pixelated";
+ nekoEl.style.left = "16px";
+ nekoEl.style.top = "16px";
+
+ document.body.appendChild(nekoEl);
+
+ document.onmousemove = (event) => {
+ mousePosX = event.clientX;
+ mousePosY = event.clientY;
+ };
+
+ window.onekoInterval = setInterval(frame, 100);
+ }
+
+ function setSprite(name, frame) {
+ const sprite = spriteSets[name][frame % spriteSets[name].length];
+ nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${
+ sprite[1] * 32
+ }px`;
+ }
+
+ function resetIdleAnimation() {
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+ }
+
+ function idle() {
+ idleTime += 1;
+
+ // every ~ 20 seconds
+ if (
+ idleTime > 10 &&
+ Math.floor(Math.random() * 200) == 0 &&
+ idleAnimation == null
+ ) {
+ idleAnimation = ["sleeping", "scratch"][
+ Math.floor(Math.random() * 2)
+ ];
+ }
+
+ switch (idleAnimation) {
+ case "sleeping":
+ if (idleAnimationFrame < 8) {
+ setSprite("tired", 0);
+ break;
+ }
+ setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
+ if (idleAnimationFrame > 192) {
+ resetIdleAnimation();
+ }
+ break;
+ case "scratch":
+ setSprite("scratch", idleAnimationFrame);
+ if (idleAnimationFrame > 9) {
+ resetIdleAnimation();
+ }
+ break;
+ default:
+ setSprite("idle", 0);
+ return;
+ }
+ idleAnimationFrame += 1;
+ }
+
+ function frame() {
+ frameCount += 1;
+ const diffX = nekoPosX - mousePosX;
+ const diffY = nekoPosY - mousePosY;
+ const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
+
+ if (distance < nekoSpeed || distance < 48) {
+ idle();
+ return;
+ }
+
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+
+ if (idleTime > 1) {
+ setSprite("alert", 0);
+ // count down after being alerted before moving
+ idleTime = Math.min(idleTime, 7);
+ idleTime -= 1;
+ return;
+ }
+
+ direction = diffY / distance > 0.5 ? "N" : "";
+ direction += diffY / distance < -0.5 ? "S" : "";
+ direction += diffX / distance > 0.5 ? "W" : "";
+ direction += diffX / distance < -0.5 ? "E" : "";
+ setSprite(direction, frameCount);
+
+ nekoPosX -= (diffX / distance) * nekoSpeed;
+ nekoPosY -= (diffY / distance) * nekoSpeed;
+
+ nekoEl.style.left = `${nekoPosX - 16}px`;
+ nekoEl.style.top = `${nekoPosY - 16}px`;
+ }
+
+ create();
+})();
diff --git a/static/js/proof-of-work.js b/static/js/proof-of-work.js
new file mode 100644
index 0000000..b4b5c57
--- /dev/null
+++ b/static/js/proof-of-work.js
@@ -0,0 +1,109 @@
+/*
+ * proof-of-work.js -- Alternative captcha based on SHA-256 proof-of-work.
+ * Copyright © 2022 - 2023 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+function makeid(length) {
+ let result = '';
+ let alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ for (let i = 0; i < length; i++) {
+ result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
+ }
+ return result;
+}
+
+async function digestMessage(message) {
+ const msgUtf8 = new TextEncoder().encode(message);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgUtf8);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
+ return hashHex;
+}
+
+async function findPrefix(hardness, nonce) {
+ while (true) {
+ let prefix = makeid(32);
+ let digestHex = await digestMessage(prefix + nonce);
+ if (digestHex.startsWith("0".repeat(hardness))) {
+ return prefix;
+ }
+ }
+}
+
+function makeRequest (method, url) {
+ return new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+ xhr.onload = function () {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resolve(xhr.response);
+ } else {
+ reject({
+ status: xhr.status,
+ statusText: xhr.statusText
+ });
+ }
+ };
+ xhr.onerror = function () {
+ reject({
+ status: xhr.status,
+ statusText: xhr.statusText
+ });
+ };
+ xhr.send();
+ });
+}
+
+function raceEndpoint() {
+ return new Promise(function (resolve, reject) {
+ makeRequest("GET", "/api/challenge/proof-of-work")
+ .then(function (data) {
+ let challengeData = JSON.parse(data);
+ findPrefix(challengeData.hardness, challengeData.nonce)
+ .then((prefix) => { resolve([prefix, challengeData["challenge-id"]]) } );
+ })
+ .catch(reject);
+ });
+}
+
+window.addEventListener("load", () => {
+ let trigger = document.getElementById("pow-trigger");
+ trigger.removeAttribute("hidden");
+ trigger.addEventListener("click", (e) => {
+ trigger.innerHTML = "Please wait...";
+ trigger.disabled = true;
+ raceEndpoint()
+ .then((result) => {
+ let resultField = document.getElementById("captcha-alt");
+ let challengeIdField = document.getElementById("captcha-alt-id");
+ resultField.value = result[0];
+ challengeIdField.value = result[1];
+ trigger.innerHTML = "Proof-of-Work completed successfully!";
+ trigger.disabled = true;
+
+ // Hide the primary captcha, too, to make it clear that it isn't necessary to complete.
+ let primaryChallenge = document.getElementById("captcha-challenge-primary");
+ primaryChallenge.hidden = true;
+ })
+ .catch((err) => {
+ console.log(err);
+ trigger.innerHTML = "Proof-of-Work failed!";
+ trigger.disabled = true;
+ });
+ e.preventDefault();
+ })
+});
diff --git a/static/js/rsvp.js b/static/js/rsvp.js
new file mode 100644
index 0000000..06cdb66
--- /dev/null
+++ b/static/js/rsvp.js
@@ -0,0 +1,206 @@
+/*
+ * rsvp.js -- Simple web-based RSVP tool.
+ * Copyright © 2022 - 2023 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+// Common nodes.
+const guestView = document.getElementById("guest-view");
+const addEntry = document.getElementById("add-entry");
+const removeEntry = document.getElementById("remove-entry");
+const submitButton = document.getElementById("submit-form");
+const form = document.getElementById("rsvp-input");
+
+// Parse out the HTML form into something we can send to the backend.
+function submitForm() {
+ let data = new FormData(form);
+ if (data.get("name").trim().length == 0) {
+ alert("You have to provide something for \"Name\".");
+ return;
+ } else if (data.get("rsvp") === null) {
+ alert("You have to choose an option for \"RSVP Status\".");
+ return;
+ }
+
+ let res = { guests: [] };
+ if (urlParams.get("i")) {
+ res["id"] = urlParams.get("i");
+ } else if (urlParams.get("r")) {
+ res["update"] = urlParams.get("r");
+ }
+ for (let pair of data.entries()) {
+ if (pair[0].startsWith("guest-")) {
+ res.guests.push(pair[1]);
+ } else if (pair[0] === "rsvp") {
+ const radioButtons = document.querySelectorAll('input[name="rsvp"]');
+ for (const radioButton of radioButtons) {
+ if (radioButton.checked) {
+ res.rsvp = radioButton.id;
+ break;
+ }
+ }
+ } else {
+ res[pair[0]] = pair[1];
+ }
+ }
+ res.guests = res.guests.join(",");
+
+ let xhr = new XMLHttpRequest();
+ xhr.open("POST", `${location.protocol}//${location.host}/api/rsvp`);
+ xhr.send(JSON.stringify(res));
+ xhr.onload = function(data) {
+ if (xhr.status != 200) {
+ alert("Failed to submit form.");
+ } else {
+ document.getElementById("rsvp").innerHTML = "";
+ let resp = JSON.parse(xhr.response);
+ let elem = document.createElement("p");
+ elem.innerHTML = "Thanks for registering! Please bookmark or save the following link:";
+ document.getElementById("rsvp").appendChild(elem);
+
+ elem = document.createElement("a");
+ elem.href = [location.protocol, '//', location.host, location.pathname].join('') + `?r=${resp.receipt}`;
+ elem.textContent = elem.href;
+ document.getElementById("rsvp").appendChild(elem);
+
+ elem = document.createElement("p");
+ elem.innerHTML = "This will enable you to edit your RSVP later.";
+ document.getElementById("rsvp").appendChild(elem);
+ }
+ }
+ xhr.onerror = function(error) {
+ throw new Error(`Request failed: ${error}`);
+ }
+}
+
+// Immediately send off an XHR to load info from the backend.
+const urlParams = new URLSearchParams(window.location.search);
+if (!urlParams.get("i") && !urlParams.get("r")) {
+ document.getElementById("loading").remove();
+ let elem = document.createElement("p");
+ elem.innerHTML = "No invitation code provided.";
+ document.getElementById("rsvp").appendChild(elem);
+} else {
+ let xhr = new XMLHttpRequest();
+ if (urlParams.get("i")) {
+ xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?i=${urlParams.get("i")}`);
+ } else {
+ xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?r=${urlParams.get("r")}`);
+ }
+ xhr.send();
+ xhr.onload = function(data) {
+ document.getElementById("loading").remove();
+ if (xhr.status != 200) {
+ let elem = document.createElement("p");
+ elem.innerHTML = "Invalid invitation code.";
+ document.getElementById("rsvp").appendChild(elem);
+ } else {
+ document.getElementById("rsvp-input").hidden = false;
+
+ let response = JSON.parse(xhr.response);
+
+ let elem = document.createElement("h1");
+ elem.innerHTML = response.title;
+ document.getElementById("event-info").appendChild(elem);
+
+ if (response.image !== "#f") {
+ elem = document.createElement("img");
+ elem.src = `data:image/png;base64,${response.image}`;
+ elem.style = "float: right; max-size: 200px; margin: 16px;";
+ document.getElementById("event-info").appendChild(elem);
+ }
+
+ elem = document.createElement("p");
+ elem.innerHTML = `Where: ${response.location}`;
+ document.getElementById("event-info").appendChild(elem);
+
+ elem = document.createElement("p");
+ elem.innerHTML = `When: ${response.date}`;
+ document.getElementById("event-info").appendChild(elem);
+
+ elem = document.createElement("p");
+ elem.innerHTML = response.description;
+ document.getElementById("event-info").appendChild(elem);
+
+ if (response.hasOwnProperty("rsvps")) {
+ elem = document.createElement("h2");
+ elem.innerHTML = "Current RSVPs";
+ document.getElementById("rsvp-global").appendChild(elem);
+
+ elem = document.createElement("table");
+ document.getElementById("rsvp-global").appendChild(elem);
+
+ for (let rsvp of response.rsvps) {
+ let container = document.createElement("tr");
+ let field = document.createElement("td");
+ field.textContent = rsvp.name;
+ container.append(field);
+ field = document.createElement("td");
+ field.textContent = rsvp.email;
+ container.append(field);
+ field = document.createElement("td");
+ field.textContent = rsvp.guests.replaceAll(',', ', ');
+ container.append(field);
+ field = document.createElement("td");
+ field.textContent = rsvp.attending;
+ container.append(field);
+ elem.appendChild(container);
+ }
+ }
+
+ // Pre-filled info if this is an edit.
+ if (response.hasOwnProperty("name")) {
+ document.getElementById("name").value = response.name;
+ }
+ if (response.hasOwnProperty("email")) {
+ document.getElementById("email").value = response.email;
+ }
+ if (response.hasOwnProperty("attending")) {
+ document.getElementById(response.attending).checked = true;
+ }
+ if (response.hasOwnProperty("guests") && response.guests.trim() != "") {
+ let i = 0;
+ for (let guest of response.guests.split(",")) {
+ let elem = document.createElement("input");
+ elem.type = "text";
+ elem.name = `guest-${i}`;
+ elem.value = guest;
+ guestView.appendChild(elem);
+ i++;
+ }
+ }
+ }
+ }
+ xhr.onerror = function(error) {
+ throw new Error(`Request failed: ${error}`);
+ }
+}
+
+// Attach event listeners to the UI.
+submitButton.addEventListener("click", submitForm);
+submitButton.addEventListener("submit", submitForm);
+addEntry.addEventListener("click", () => {
+ let i = guestView.childNodes.length;
+ let elem = document.createElement("input");
+ elem.type = "text";
+ elem.name = `guest-${i}`;
+ guestView.appendChild(elem);
+});
+removeEntry.addEventListener("click", () => {
+ if (guestView.lastChild) {
+ guestView.lastChild.remove();
+ }
+});
diff --git a/static/js/section-folds.js b/static/js/section-folds.js
new file mode 100644
index 0000000..9440264
--- /dev/null
+++ b/static/js/section-folds.js
@@ -0,0 +1,66 @@
+/*
+ * section-folds.js -- Parse out sections to hide behind an "unfold" dialog
+ * Copyright © 2022 - 2023 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+function handleFolds() {
+ function displayFold(fold, foldText, frontMatterText) {
+ foldText.innerHTML = `⬇️ ${frontMatterText}`;
+ fold.classList.remove("fold-hidden");
+
+ }
+
+ function hideFold(fold, foldText, frontMatterText) {
+ foldText.innerHTML = `➡️ Folded ${frontMatterText}`;
+ fold.classList.add("fold-hidden");
+ }
+
+ let folds = document.querySelectorAll(".section-fold");
+
+ for (let fold of folds) {
+ // The first thing we'll do is generate the "front matter" for the fold, so
+ // that the reader knows what lies beneath the fold.
+ let frontMatterText = `Section on "${fold.dataset.name}"`;
+ let foldFrontMatter = document.createElement("div");
+
+ // The text will be inside of an anchor, which we'll hook onto using an
+ // event listener.
+ let foldAnchor = document.createElement("a");
+ let foldText = document.createElement("p");
+ foldAnchor.appendChild(foldText);
+ fold.parentNode.insertBefore(foldAnchor, fold.nextSibling);
+
+ // The fold will start off in the "hidden" state.
+ let toggle = false;
+ hideFold(fold, foldText, frontMatterText);
+
+ // But clicking on the "front matter" will cause the state of the fold
+ // to toggle, first to "visible", and then back to "hidden".
+ foldAnchor.addEventListener('click', () => {
+ toggle = !toggle;
+ if (toggle) {
+ displayFold(fold, foldText, frontMatterText);
+ } else {
+ hideFold(fold, foldText, frontMatterText);
+ }
+ });
+ }
+
+}
+
+// Defer parsing all of the "folds" until the document has completely loaded.
+document.addEventListener("DOMContentLoaded", handleFolds);
diff --git a/static/js/webmention.js b/static/js/webmention.js
new file mode 100644
index 0000000..d280d4d
--- /dev/null
+++ b/static/js/webmention.js
@@ -0,0 +1,229 @@
+/*
+ * webmention.js -- Fetch and display mentions from webmention.io.
+ * Copyright © 2019 - 2023 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+function buildApiUri() {
+ const ref = "jakob.space";
+ const slug = /[^\/]*(?=\.html)/.exec(window.location.href);
+ const endpoints = [
+ `http://${ref}/${slug}.html`,
+ `https://${ref}/${slug}.html`,
+ `http://${ref}/blog/${slug}.html`,
+ `https://${ref}/blog/${slug}.html`,
+ ];
+ return ["https://webmention.io/api/mentions.jf2?per-page=200&page=0"].concat(
+ endpoints.map((endpoint) => {
+ return `target[]=${endpoint}`;
+ }).join("&")
+ ).join("");
+}
+
+function getData(url, callback) {
+ if (fetch) {
+ fetch(url).then(function(response) {
+ if (response.status >= 200 && response.status < 300) {
+ return Promise.resolve(response);
+ } else {
+ return Promise.reject(new Error(`Request failed: ${response.statusText}`));
+ }
+ }).then(function(response) {
+ return response.json();
+ }).then(callback);
+ } else {
+ let xhr = new XMLHttpRequest();
+ xhr.onload = function(data) {
+ callback(JSON.parse(data));
+ }
+ xhr.onerror = function(error) {
+ throw new Error(`Request failed: ${error}`);
+ }
+ }
+}
+
+function makeComment(comment) {
+ const strip = (uri) => {
+ const sep = "://";
+ return uri.substring(uri.indexOf(sep) + sep.length);
+ };
+
+ const element = (type, attributes) => {
+ let res = document.createElement(type);
+ for (const attribute in attributes) {
+ res.setAttribute(attribute, attributes[attribute]);
+ }
+ return res;
+ };
+
+ // Create the h-card section.
+ let avatar = element("img", {
+ "class": "u-photo",
+ "src": comment.url.startsWith("https://lobste.rs/")
+ ? "/static/image/lobsters.png"
+ : comment.author.photo || "/static/image/default-icon.png"
+ });
+
+ let authorName = element("a", {
+ "class": "p-name u-url",
+ "href": comment.author.url
+ });
+ authorName.textContent = comment.author.name;
+
+ let authorURI = element("a", {
+ "class": "author_url",
+ "href": comment.author.url
+ });
+ authorURI.textContent = strip(comment.author.url);
+
+ let hCardContainer = element("div", {
+ "class": "p-author h-card author"
+ });
+ hCardContainer.appendChild(avatar);
+ hCardContainer.appendChild(authorName);
+ hCardContainer.appendChild(authorURI);
+
+ // Create the content section.
+ let contentContainer = element("div", {
+ "class": "e-content p-name comment-content",
+ });
+
+ if (comment.hasOwnProperty("content")) {
+ contentContainer.textContent = comment.content.text;
+ } else {
+ let em = element("em", {});
+ if (comment.hasOwnProperty("wm-property")) {
+ if ("repost-of" === comment["wm-property"]) {
+ em.textContent = "Reposted this!";
+ } else if ("like-of" === comment["wm-property"]) {
+ em.textContent = "Favorited this!";
+ } else {
+ em.textContent = "No text provided :(";
+ }
+ } else {
+ em.textContent = "No text provided :(";
+ }
+ contentContainer.appendChild(em);
+ }
+
+ // Create the metaline section.
+ let pubTime = comment.published || comment["wm-received"];
+ let time = element("time", {
+ "class": "dt-published",
+ "datetime": pubTime,
+ });
+ time.textContent = (new Date(pubTime)).toString();
+
+ let linkBack = element("a", {
+ "class": "u-url",
+ "href": comment.url
+ });
+ linkBack.appendChild(time);
+
+ let metalineContainer = element("div", {
+ "class": "metaline"
+ });
+ metalineContainer.appendChild(linkBack);
+
+ let sourceIcon = document.createElement("img");
+ sourceIcon.classList.add("comment-source-identifier")
+ sourceIcon.src = "/static/image/webmention-logo.png";
+
+ // Put it all together.
+ let wrapper = element("li", {
+ "class": "p-comment h-cite comment comment-source-webmention",
+ });
+ wrapper.appendChild(sourceIcon);
+ wrapper.appendChild(hCardContainer);
+ wrapper.appendChild(contentContainer);
+ wrapper.appendChild(metalineContainer);
+
+ return wrapper;
+}
+
+function makeInteraction(ent) {
+ const strip = (uri) => {
+ const sep = "://";
+ return uri.substring(uri.indexOf(sep) + sep.length);
+ };
+
+ const element = (type, attributes) => {
+ let res = document.createElement(type);
+ for (const attribute in attributes) {
+ res.setAttribute(attribute, attributes[attribute]);
+ }
+ return res;
+ };
+
+ // Create the h-card section.
+ let avatar = element("img", {
+ "class": "u-photo",
+ "src": ent.url.startsWith("https://lobste.rs/")
+ ? "/static/image/lobsters.png"
+ : ent.author.photo || "/static/image/default-icon.png"
+ });
+ let avatarContainer = element("a", {
+ "href": ent.author.url
+ });
+ avatarContainer.appendChild(avatar);
+
+ // Create the content section.
+ let contentContainer = element("div", {
+ "class": "e-content p-name comment-content",
+ });
+ let em = element("em", {});
+ if ("repost-of" === ent["wm-property"]) {
+ em.textContent = "Reposted this!";
+ } else if ("like-of" === ent["wm-property"]) {
+ em.textContent = "Favorited this!";
+ } else {
+ return null;
+ }
+ contentContainer.appendChild(em);
+
+ let sourceIcon = document.createElement("img");
+ sourceIcon.classList.add("comment-source-identifier")
+ sourceIcon.src = "/static/image/webmention-logo.png";
+
+ // Put it all together.
+ let wrapper = element("li", {
+ "class": "p-comment h-cite interaction comment-source-webmention",
+ });
+ wrapper.appendChild(avatarContainer);
+ wrapper.appendChild(contentContainer);
+ wrapper.appendChild(sourceIcon);
+
+ return wrapper;
+}
+
+function makeEntry(ent) {
+ if (ent.hasOwnProperty("wm-property")) {
+ if (ent.hasOwnProperty("content") && ent["wm-property"] !== "repost-of") {
+ return makeComment(ent);
+ } else {
+ return makeInteraction(ent);
+ }
+ }
+ return null;
+}
+
+getData(buildApiUri(), (json) => {
+ json.children
+ .map(makeEntry)
+ .filter((x) => x !== null)
+ .forEach((elem) => {
+ document.getElementById("webmention-container").appendChild(elem);
+ })});