hswaw/site: wip new layout

Change-Id: I4da3a668429dee42c7292accb9e24b93703f1538
diff --git a/hswaw/site/BUILD.bazel b/hswaw/site/BUILD.bazel
index 5cb3b99..fa2769e 100644
--- a/hswaw/site/BUILD.bazel
+++ b/hswaw/site/BUILD.bazel
@@ -5,6 +5,7 @@
     name = "go_default_library",
     srcs = [
         "at.go",
+        "events.go",
         "feeds.go",
         "main.go",
         "spaceapi.go",
@@ -41,5 +42,5 @@
     format = "Docker",
     registry = "registry.k0.hswaw.net",
     repository = "q3k/hswaw-site",
-    tag = "1622585979-{STABLE_GIT_COMMIT}",
+    tag = "1626124964-{STABLE_GIT_COMMIT}",
 )
diff --git a/hswaw/site/calendar/event.go b/hswaw/site/calendar/event.go
index 19a916b..141e4a6 100644
--- a/hswaw/site/calendar/event.go
+++ b/hswaw/site/calendar/event.go
@@ -19,6 +19,8 @@
 	UID string
 	// Summary is the 'title' of the event, usually a short one-liner.
 	Summary string
+	// Full description of event. Might contain multiple lines of test.
+	Description string
 	// Start and End of the events, potentially whole-day dates. See EventTime
 	// for more information.
 	// If Start is WholeDay then so is End, and vice-versa.
diff --git a/hswaw/site/calendar/load.go b/hswaw/site/calendar/load.go
index 5b36b9c..f9ae146 100644
--- a/hswaw/site/calendar/load.go
+++ b/hswaw/site/calendar/load.go
@@ -6,6 +6,7 @@
 	"io"
 	"net/http"
 	"sort"
+	"strings"
 	"time"
 	_ "time/tzdata"
 
@@ -57,6 +58,13 @@
 		}
 		summary := summaryProp.Value
 
+		var description string
+		descriptionProp := event.GetProperty(ics.ComponentPropertyDescription)
+		if descriptionProp != nil && descriptionProp.Value != "" {
+			// The ICS/iCal description has escaped newlines. Undo that.
+			description = strings.ReplaceAll(descriptionProp.Value, `\n`, "\n")
+		}
+
 		status := event.GetProperty(ics.ComponentPropertyStatus)
 		tentative := false
 		if status != nil {
@@ -87,11 +95,12 @@
 		}
 
 		u := &UpcomingEvent{
-			UID:       uid,
-			Summary:   summary,
-			Start:     start,
-			End:       end,
-			Tentative: tentative,
+			UID:         uid,
+			Summary:     summary,
+			Description: description,
+			Start:       start,
+			End:         end,
+			Tentative:   tentative,
 		}
 		if u.Elapsed(now) {
 			continue
diff --git a/hswaw/site/calendar/load_test.go b/hswaw/site/calendar/load_test.go
index a07f134..1b2945a 100644
--- a/hswaw/site/calendar/load_test.go
+++ b/hswaw/site/calendar/load_test.go
@@ -22,8 +22,9 @@
 
 	want := []*UpcomingEvent{
 		{
-			UID:     "65cd51ba-2fd7-475e-a274-61d19c186b66",
-			Summary: "test event please ignore",
+			UID:         "65cd51ba-2fd7-475e-a274-61d19c186b66",
+			Summary:     "test event please ignore",
+			Description: "I am a description",
 			Start: &EventTime{
 				Time: time.Unix(1626091200, 0),
 			},
@@ -32,8 +33,9 @@
 			},
 		},
 		{
-			UID:     "2f874784-1e09-4cdc-8ae6-185c9ee36be0",
-			Summary: "many days",
+			UID:         "2f874784-1e09-4cdc-8ae6-185c9ee36be0",
+			Summary:     "many days",
+			Description: "I am a multiline\n\ndescription\n\nwith a link: https://example.com/foo\n\nbarfoo",
 			Start: &EventTime{
 				Time:     time.Unix(1626134400, 0),
 				WholeDay: true,
diff --git a/hswaw/site/events.go b/hswaw/site/events.go
new file mode 100644
index 0000000..26d4866
--- /dev/null
+++ b/hswaw/site/events.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+	"context"
+	"time"
+
+	"code.hackerspace.pl/hscloud/hswaw/site/calendar"
+	"github.com/golang/glog"
+)
+
+func (s *service) eventsWorker(ctx context.Context) {
+	get := func() {
+		events, err := calendar.GetUpcomingEvents(ctx, time.Now())
+		if err != nil {
+			glog.Errorf("Geting events failed: %v", err)
+			return
+		}
+
+		s.eventsMu.Lock()
+		s.events = events
+		s.eventsMu.Unlock()
+	}
+	// Perform initial fetch.
+	get()
+
+	// .. and update very minute.
+	t := time.NewTicker(time.Minute)
+	defer t.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-t.C:
+			get()
+		}
+	}
+}
+
+func (s *service) getEvents() []*calendar.UpcomingEvent {
+	s.eventsMu.RLock()
+	events := s.events
+	s.eventsMu.RUnlock()
+	return events
+}
diff --git a/hswaw/site/main.go b/hswaw/site/main.go
index c4437bc..c1cc1e3 100644
--- a/hswaw/site/main.go
+++ b/hswaw/site/main.go
@@ -3,15 +3,18 @@
 import (
 	"flag"
 	"fmt"
+	"math/rand"
 	"mime"
 	"net/http"
 	"regexp"
 	"strings"
 	"sync"
+	"time"
 
 	"code.hackerspace.pl/hscloud/go/mirko"
 	"github.com/golang/glog"
 
+	"code.hackerspace.pl/hscloud/hswaw/site/calendar"
 	"code.hackerspace.pl/hscloud/hswaw/site/static"
 )
 
@@ -25,12 +28,20 @@
 	feeds map[string]*atomFeed
 	// feedsMu locks the feeds field.
 	feedsMu sync.RWMutex
+
+	// events is a list of upcoming events, sorted so the first event is the
+	// one that will happen the soonests.
+	events []*calendar.UpcomingEvent
+	// eventsMu locks the events field.
+	eventsMu sync.RWMutex
 }
 
 func main() {
 	flag.StringVar(&flagSitePublic, "site_public", "0.0.0.0:8080", "Address at which to serve public HTTP requests")
 	flag.Parse()
 
+	rand.Seed(time.Now().UnixNano())
+
 	mi := mirko.New()
 	if err := mi.Listen(); err != nil {
 		glog.Exitf("Listen failed: %v", err)
@@ -38,6 +49,7 @@
 
 	s := &service{}
 	go s.feedWorker(mi.Context())
+	go s.eventsWorker(mi.Context())
 
 	mux := http.NewServeMux()
 	s.registerHTTP(mux)
@@ -103,5 +115,7 @@
 func (s *service) registerHTTP(mux *http.ServeMux) {
 	mux.HandleFunc("/static/", s.handleHTTPStatic)
 	mux.HandleFunc("/spaceapi", s.handleSpaceAPI)
+	mux.HandleFunc("/events.json", s.handleJSONEvents)
+	mux.HandleFunc("/event/", s.handleEvent)
 	mux.HandleFunc("/", s.handleIndex)
 }
diff --git a/hswaw/site/static/BUILD.bazel b/hswaw/site/static/BUILD.bazel
index 7fdce2d..fc0f04d 100644
--- a/hswaw/site/static/BUILD.bazel
+++ b/hswaw/site/static/BUILD.bazel
@@ -4,9 +4,12 @@
 go_embed_data(
     name = "static",
     srcs = [
+        "animations.js",
         "landing.css",
-        "syrenka.png",
+        "led.js",
+        "neon-syrenka.svg",
         "@com_npmjs_leaflet//:distfiles",
+        "space.jpg",
     ],
     package = "static",
 )
diff --git a/hswaw/site/static/animations.js b/hswaw/site/static/animations.js
new file mode 100644
index 0000000..77d9562
--- /dev/null
+++ b/hswaw/site/static/animations.js
@@ -0,0 +1,273 @@
+// To add your own animation, extend 'Animation' and implement draw(), then add
+// your animation's class name to the list at the bottom of the script.
+
+class Animation {
+    // The constructor for Animation is called by the site rendering code when
+    // the site loads, so it should be fairly fast. Any delay causes the LED
+    // panel to take longer to load.
+    constructor(nx, ny) {
+        // LED array, indexed by x then y.
+        let leds = new Array(nx);
+        for (let x = 0; x < nx; x++) {
+            leds[x] = new Array(ny);
+            for (let y = 0; y < ny; y++) {
+                leds[x][y] = [0.0, 0.0, 0.0];
+            }
+        }
+        this.leds = leds;
+
+        // Number of LEDs, X and Y.
+        this.nx = nx;
+        this.ny = ny;
+    }
+
+    // Helper function that converts from HSV to RGB, can be used by your draw
+    // code.
+    // H, S and V values must be [0..1].
+    hsv2rgb(h, s, v) {
+        const i = Math.floor(h * 6);
+        const f = h * 6 - i;
+        const p = v * (1 - s);
+        const q = v * (1 - f * s);
+        const t = v * (1 - (1 - f) * s);
+
+        let r, g, b;
+        switch (i % 6) {
+            case 0: r = v, g = t, b = p; break;
+            case 1: r = q, g = v, b = p; break;
+            case 2: r = p, g = v, b = t; break;
+            case 3: r = p, g = q, b = v; break;
+            case 4: r = t, g = p, b = v; break;
+            case 5: r = v, g = p, b = q; break;
+        }
+        return [r, g, b];
+    }
+
+    draw(ts) {
+        // Implement your animation here.
+        // The 'ts' argument is a timestamp in seconds, floating point, of the
+        // frame being drawn.
+        //
+        // Your implementation should write to this.leds, which is two
+        // dimensional array containing [r,g,b] values. Colour values are [0..1].
+        //
+        // X coordinates are [0 .. this.nx), Y coordinates are [0 .. this.ny).
+        // The coordinate system is with X==Y==0 in the top-left part of the
+        // display.
+        //
+        // For example, for a 3x3 LED display the coordinates are as follors:
+        //
+        //  (x:0 y:0)  (x:1 y:0)  (x:2  y:0)
+        //  (x:0 y:1)  (x:1 y:1)  (x:2  y:1)
+        //  (x:0 y:2)  (x:1 y:2)  (x:2  y:2)
+        //
+        // The LED array (this.leds) is indexed by X first and Y second.
+        //
+        // For example, to set the LED red at coordinates x:1 y:2:
+        //
+        // this.leds[1][2] = [1.0, 0.0, 0.0];
+    }
+}
+
+// 'Snake' chase animation, a simple RGB chase that goes around in a zigzag.
+// By q3k.
+class SnakeChase extends Animation {
+    draw(ts) {
+        const nx = this.nx;
+        const ny = this.ny;
+        // Iterate over all pixels column-wise.
+        for (let i = 0; i < (nx*ny); i++) {
+            let x = Math.floor(i / ny);
+            let y = i % ny;
+
+            // Flip every second row to get the 'snaking'/'zigzag' effect
+            // during iteration.
+            if (x % 2 == 0) {
+                y = ny - (y + 1);
+            }
+
+            // Pick a hue for every pixel.
+            let h = (i / (nx*ny) * 10) + (ts/2);
+            h = h % 1;
+
+            // Convert to RGB.
+            let c = this.hsv2rgb(h, 1, 1);
+
+            // Poke.
+            this.leds[x][y] = c;
+        }
+    }
+}
+
+// Game of life on a torus, with random state. If cycles or stalls are
+// detected, the simulation is restarted.
+// By q3k.
+class Life extends Animation {
+    draw(ts) {
+        // Generate state if needed.
+        if (this.state === undefined) {
+            this.generateState();
+        }
+
+        // Step simulation every so often.
+        if (this.nextStep === undefined || this.nextStep < ts) {
+            if (this.nextStep !== undefined) {
+                this.step();
+                this.recordState();
+            }
+            // 10 steps per second.
+            this.nextStep = ts + 1.0/10;
+        }
+
+        if (this.shouldRestart(ts)) {
+            this.generateState();
+        }
+
+        // Render state into LED matrix.
+        for (let x = 0; x  < this.nx; x++) {
+            for (let y = 0; y < this.ny; y++) {
+                // Turn on and decay smoothly.
+                let [r, g, b] = this.leds[x][y];
+                if (this.state[x][y]) {
+                    r += 0.5;
+                    g += 0.5;
+                    b += 0.5;
+                } else {
+                    r -= 0.05;
+                    g -= 0.05;
+                    b -= 0.05;
+                }
+                r = Math.min(Math.max(r, 0.0), 1.0);
+                g = Math.min(Math.max(g, 0.0), 1.0);
+                b = Math.min(Math.max(b, 0.0), 1.0);
+                this.leds[x][y] = [r, g, b];
+            }
+        }
+    }
+
+    // recordState records the current state of the simulation within a
+    // 3-element FIFO. This data is used to detect 'stuck' simulations. Any
+    // time there is something repeating within the 3-element FIFO, it means
+    // we're in some boring loop or terminating step, and shouldRestart will
+    // then schedule a simulation restart.
+    recordState() {
+        if (this.recorded === undefined) {
+            this.recorded = [];
+        }
+        // Serialize state into string of 1 and 0.
+        const serialized = this.state.map((column) => { 
+            return column.map((value) => value ? "1" : "0").join("");
+        }).join("");
+        this.recorded.push(serialized);
+
+        // Ensure there's not more then 3 recorded state;
+        while (this.recorded.length > 3) {
+            this.recorded.shift();
+        }
+    }
+
+    // shouldRestart looks at the recorded state of simulation frames, and
+    // ensures that there isn't anything repeated within the recorded data. If
+    // so, it schedules a restart of the simulation in 5 seconds.
+    shouldRestart(ts) {
+        // Nothing to do if we have no recorded data.
+        if (this.recorded === undefined) {
+            return false;
+        }
+
+        // If we have a deadline for restarting set already, just obey that and
+        // return true when it expires.
+        if (this.restartDeadline !== undefined) {
+            if (this.restartDeadline < ts) {
+                this.restartDeadline = undefined;
+                return true;
+            }
+            return false;
+        }
+
+        // Otherwise, look for repeat data in the recorded history. If anything
+        // is recorded, schedule a restart deadline in 5 seconds.
+        let s = new Set();
+
+        let restart = false;
+        for (let key of this.recorded) {
+            if (s.has(key)) {
+                restart = true;
+                break;
+            }
+            s.add(key);
+        }
+        if (restart) {
+            console.log("shouldRestart detected restart condition, scheduling restart...");
+            this.restartDeadline = ts + 2;
+        }
+    }
+
+    // generateState builds the initial randomized state of the simulation.
+    generateState() {
+        this.state = new Array();
+        for (let x = 0; x < this.nx; x++) {
+            this.state.push(new Array());
+            for (let y = 0; y < this.ny; y++) {
+                this.state[x][y] = Math.random() > 0.5;
+            }
+        }
+        this.recorded = [];
+    }
+
+    // step runs a simulation step for the game of life board.
+    step() {
+        let next = new Array();
+        for (let x = 0; x < this.nx; x++) {
+            next.push(new Array());
+            for (let y = 0; y < this.ny; y++) {
+                next[x][y] = this.nextFor(x, y);
+            }
+        }
+        this.state = next;
+    }
+
+    // nextFor runs a simulation step for a game of life cell at given
+    // coordinates.
+    nextFor(x, y) {
+        let current = this.state[x][y];
+        // Build coordinates of neighbors, wrapped around (effectively a
+        // torus).
+        let neighbors = [
+            [x-1, y-1], [x, y-1], [x+1, y-1],
+            [x-1, y  ],           [x+1, y  ],
+            [x-1, y+1], [x, y+1], [x+1, y+1],
+        ].map(([x, y]) => {
+            x = x % this.nx;
+            y = y % this.ny;
+            if (x < 0) {
+                x += this.nx;
+            }
+            if (y < 0) {
+                y += this.ny;
+            }
+            return [x, y];
+        });
+        // Count number of live and dead neighbours.
+        const live = neighbors.filter(([x, y]) => { return this.state[x][y]; }).length;
+
+        if (current) {
+            if (live < 2 || live > 3) {
+                current = false;
+            }
+        } else {
+            if (live == 3) {
+                current = true;
+            }
+        }
+
+        return current;
+    }
+}
+
+// Add your animations here:
+export const animations = [
+    Life,
+    SnakeChase,
+];
+
diff --git a/hswaw/site/static/landing.css b/hswaw/site/static/landing.css
index 3ddae1a..3ad4cd5 100644
--- a/hswaw/site/static/landing.css
+++ b/hswaw/site/static/landing.css
@@ -1,100 +1,249 @@
+:root {
+    --primary: #7347d9ff;
+    --primary100: #cfbff1;
+    --secondary: #d947adff;
+    --secondary50: #fae2f0;
+    --darkbgaccent: #1a1622ff;
+    --darkbg: #121212ff;
+    --darkbgalpha: #121212f8;
+}
+
+html {
+    min-height: 100%;
+}
+
 body {
+    min-height: 100%;
+
     margin: 0;
     padding: 0;
-    background-color: #444;
     color: #fffdf3;
-    font-weight: 100;
-    font-family: 'Lato', sans-serif;
-    font-size: 18px;
+    font-weight: 400;
+    font-family: 'Courier Prime', monospace;
+    font-size: 20px;
+    line-height: 150%;
+
+    background-color: var(--darkbgaccent);
+}
+
+@media screen and (max-width: 1000px) {
+    body {
+        font-size: 18px;
+    }
+}
+
+#ledsFloater {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    min-height: 100%;
+    overflow-x: hidden;
+    z-index: -11;
+}
+
+#ledsWrapper {
+    float: left; /* oh god */
+    width: 100%;
+    min-height: 100%;
+}
+
+#leds {
+    width: max(60vw, 600px);
+    height: max(60vw, 600px);
+    transform: rotate(-15deg);
+    position: relative;
+    top: min(-10vw, -100px);
+    left: min(-10vw, -100px);
+    z-index: -10;
 }
 
 #page {
-    max-width: 75rem;
-    margin: auto;
-    padding-top: 2rem;
-    padding: 1rem;
+    max-width: 60rem;
+    margin: 6em auto 2em auto;
+    background-color: var(--darkbgalpha);
 
     display: flex;
     flex-direction: column;
 }
 
+@media screen and (max-width: 1000px) {
+    #page {
+        background-color: #121212f0;
+    }
+}
+
+.about img {
+    width: 100%;
+    display: block;
+    margin: 0 auto;
+}
+
 .top {
     display: flex;
     flex-direction: row;
-    margin-bottom: 1rem;
+    flex-flow: row wrap;
+    margin: 2em 0 1em 0;
+    justify-content: center;
 }
 
-.top .logo {
-    display: flex;
-    flex-direction: row;
-    flex-grow: 1;
-    justify-content: right;
+@media screen and (max-width: 1000px) {
+    .top {
+        margin: 1em 0 0 0;
+    }
 }
 
 .top .logo img {
-    margin-top: 2rem;
-    max-height: 25rem
+    max-height: 15rem;
 }
 
-.top .mapcontainer {
-    flex-grow: 1;
-    min-width: 35%;
+@media screen and (max-width: 1000px) {
+    .top .logo img {
+        max-height: 8rem;
+    }
 }
 
+.top .type {
+    max-width: 13em;
+    font-size: min(35px, 4vw);
+    line-height: 0.9;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+}
+
+.top .type h1 {
+    padding: 0 0 0.5em 0.2em;
+    font-family: 'Noto Sans', sans-serif;
+    color: #fff;
+    text-shadow: 0.05em 0.05em var(--secondary);
+}
+
+
 #map {
-    margin-bottom: 1rem;
-    height: 16rem;
+    height: 28em;
 }
 
 
-.logo h1 {
-    display: block;
-    max-width: 20rem;
-    text-align: center;
-    font-size: 52px;
-    margin-right: 8rem;
-    padding-top: 5rem;
+.quicklinks {
+    font-size: 16px;
+    background-color: rgba(255, 255, 255, 0.05);
+}
+
+@media screen and (max-width: 1000px) {
+    .quicklinks {
+        font-size: 14px;
+     }
+}
+
+.quicklinks ul {
+    padding: 0;
+    margin: 0;
+    display: flex;
+    flex-direction: row;
+    flex-flow: row wrap;
+    justify-content: center;
+    font-family: 'Noto Sans', sans-serif;
+}
+
+.quicklinks ul li {
+    display: flex;
+}
+
+.quicklinks ul li:not(.left) {
+}
+
+.quicklinks ul li.left {
+    flex-grow: 1;
+    font-style: italic;
+}
+
+.quicklinks a {
+    text-decoration: none;
+    padding: 0.8rem 1rem;
+    color: #fff;
+}
+.quicklinks a:hover {
+    color: #fff;
+}
+
+.quicklinks li:not(.left) a:hover {
+    background-color: rgba(255, 255, 255, 0.05);
 }
 
 .covid {
     padding: 1rem 2rem;
-    background-color: #9f0000;
-}
-
-.covid span {
-    font-size: 20px;
+    background-color: rgba(150, 0, 0, 0.8);
+    font-size: 18px;
     font-style: italic;
 }
 
 .bottom {
     display: flex;
-    flex-direction: row;
+    flex-direction: column;
+    padding: 1em 1em 0 1em;
+}
+
+@media screen and (max-width: 1000px) {
+    .bottom {
+        padding: 2em 1em 0 1em;
+    }
 }
 
 .bottom .about {
-    padding: 1rem 1rem 1rem 2rem;
+    padding: 1rem 2em 3rem 2em;
 }
 
-.bottom .blog {
-    padding: 1rem 2rem 1rem 1rem;
-    flex-grow: 1;
-    min-width: 40%;
+@media screen and (max-width: 1000px) {
+    .bottom .about {
+        padding: 0rem 0em 1rem 0em;
+    }
+}
+
+
+.bottom .about li + li {
+    margin-top: 0.5em;
 }
 
 p {
-    line-height: 150%;
-    text-align: justify;
+    text-align: left;
+    color: #eee;
 }
 
-h1 {
-    font-size: 30px;
-}
 h2 {
-    font-size: 20px;
+    margin-bottom: 0;
 }
 
-h1, h2, h3, h4 {
-    font-family: 'Allerta', sans-serif;
+@media screen and (max-width: 1000px) {
+    h2 {
+        margin: 0;
+    }
+}
+
+* + h2 {
+    margin: 2rem 0 0 0;
+}
+
+h2 + * {
+    margin: 1rem 0 0 0;
+}
+
+h2 {
+    font-size: 26px;
+    display: inline-block;
+    font-family: 'Noto Sans', sans-serif;
+}
+h2:after {
+    content: " ";
+    display: block;
+    background-color: var(--secondary);
+    height: 0.15em;
+    width: 100%;
+    margin-top: 0.1em;
+    margin-left: 0.3em
+}
+h3 {
+    font-size: 20px;
 }
 
 pre {
@@ -103,7 +252,13 @@
 }
 
 a {
-    color: #fffdf3;
+    text-decoration: underline;
+    text-decoration-color: var(--primary100);
+    color: #fff;
+}
+
+a:hover {
+    color: var(--primary100);
 }
 
 b {
@@ -114,29 +269,22 @@
     padding: 0 0 0 1em;
 }
 
+@media screen and (max-width: 1000px) {
+    ul {
+        padding: 0;
+    }
+}
+
 li {
     list-style: none;
 }
 
 li i {
-    font-size: 0.8em;
-}
-
-#background-logo {
-    position: absolute;
-    width: 100%;
-    height: 100%;
-    z-index: -10;
-}
-
-#background-logo img {
-    opacity: 3%;
-    margin-top: 2%;
-    margin-left: 5%;
+    font-size: 0.9em;
 }
 
 #footer {
-    margin-top: 2rem;
+    margin: 1rem 0 0 0;
     font-size: 0.8rem;
     opacity: 60%;
 }
diff --git a/hswaw/site/static/led.js b/hswaw/site/static/led.js
new file mode 100644
index 0000000..61580f0
--- /dev/null
+++ b/hswaw/site/static/led.js
@@ -0,0 +1,72 @@
+import { animations } from "./animations.js";
+
+class CanvasRenderer {
+    static WIDTH = 1024;
+    static HEIGHT = 1024;
+
+    constructor() {
+        const ledDiv = document.querySelector("#leds");
+        let canvas = document.createElement("canvas");
+        canvas.style.width = "100%";
+        canvas.style.height = "100%";
+        canvas.width = CanvasRenderer.WIDTH;
+        canvas.height = CanvasRenderer.HEIGHT;
+        ledDiv.appendChild(canvas);
+        ledDiv.style.backgroundColor = "#00000000";
+        let context = canvas.getContext('2d');
+
+        this.canvas = canvas;
+        this.context = context;
+    }
+
+    render(animation) {
+        const canvas = this.canvas;
+        const context = this.context;
+        const leds = animation.leds;
+        const nx = animation.nx;
+        const ny = animation.ny;
+
+        const xoff = CanvasRenderer.WIDTH / (nx + 1);
+        const yoff = CanvasRenderer.HEIGHT / (ny + 1);
+        const d = xoff * 0.7;
+
+        context.clearRect(0, 0, canvas.width, canvas.height);
+        for (let x = 0; x < nx; x++) {
+            for (let y = 0; y < ny; y++) {
+                const cx = (x + 1) * xoff
+                const cy = (y + 1) * yoff
+
+                const rgb = leds[x][y];
+                const r = Math.max(rgb[0] * 256, 0x1a);
+                const g = Math.max(rgb[1] * 256, 0x16);
+                const b = Math.max(rgb[2] * 256, 0x22);
+                const color = `rgba(${r}, ${g}, ${b})`;
+
+                context.beginPath();
+                context.arc(cx, cy, d/2, 0, 2 * Math.PI, false);
+                context.fillStyle = color;
+                context.fill();
+            }
+        }
+    }
+}
+
+window.addEventListener("load", () => {
+    const animationClass = animations[Math.floor(Math.random() * animations.length)];
+    console.log(`Picked LED animation: ${animationClass.name}`);
+
+    let renderer = new CanvasRenderer();
+    let animation = new animationClass(16, 16);
+
+    let step = (hrts) => {
+        // Run animation logic.
+        animation.draw(hrts / 1000);
+
+        // Draw LEDs.
+        renderer.render(animation);
+
+        // Schedule next frame.
+        window.requestAnimationFrame(step);
+    }
+    window.requestAnimationFrame(step);
+});
diff --git a/hswaw/site/static/neon-syrenka.svg b/hswaw/site/static/neon-syrenka.svg
new file mode 100644
index 0000000..72fd114
--- /dev/null
+++ b/hswaw/site/static/neon-syrenka.svg
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   width="210mm"
+   height="297mm"
+   viewBox="0 0 210 297"
+   version="1.1"
+   id="svg4661">
+  <defs
+     id="defs4655" />
+  <metadata
+     id="metadata4658">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1">
+    <path
+       d="m 192.769,218.96976 c -5.05369,5.54002 -13.21138,7.72603 -15.07391,11.71992 -8.93948,19.18306 -14.72499,27.8945 -26.11805,36.04284 -9.64327,6.90351 -22.29975,14.53301 -41.53166,16.23734 -15.446242,1.37282 -25.325723,-0.57701 -39.714462,-6.04376 -6.480056,-2.46405 -22.834899,-13.20676 -26.884639,-18.57343 -5.571452,-7.38519 -10.615892,-12.18879 -13.206788,-21.36792 -2.821194,-10.00279 -5.421339,-19.68678 -5.638876,-30.29335 -0.109881,-5.44001 0.0998,-10.1715 0.66777,-16.12451 0.872511,-2.20812 2.29533,-4.31735 4.19867,-5.7844 0.191249,-0.14535 0.05911,-0.0685 0.178383,-1.75434 0.158215,-2.25464 0.853873,-2.82237 0.972586,-3.40408 0.172123,-0.84577 -0.391016,-9.71658 0.486293,-13.61509 1.831152,-8.12622 3.93113,-13.91058 5.712245,-21.07235 0.09528,-0.38629 0.145349,-0.67361 0.122052,-0.80975 -5.28065,-6.14384 -7.902911,-4.01716 -8.389169,-20.69545 l 0.77713,-0.83183 c 2.681617,-1.21338 9.757291,-4.34405 12.556369,-6.07518 m 35.328539,27.98405 -2.649,2.08593 c -6.162515,1.59968 -13.342888,-1.74971 -15.074,-8.26583 m -27.425629,49.82076 c 2.708323,4.40811 4.644244,8.17164 7.534114,12.40171 1.231988,1.79975 5.603999,8.5125 8.026109,8.5125 1.44031,0 2.767714,-0.0546 3.953211,-0.33625 1.809072,-0.42815 12.079396,-9.49789 13.710468,-18.87828 0.618949,-3.80424 3.563481,-8.6486 7.294461,-9.96554 0.994701,-0.35482 2.958476,-1.06331 4.135802,-0.48629 l 3.499465,0.73523 m 76.205196,48.32704 c -1.82187,5.47137 -8.10181,8.90686 -14.56907,11.03351 -7.91682,2.60365 -15.7103,4.04393 -23.82608,1.39493 -3.27727,-1.07266 -6.44052,-2.65836 -9.47581,-4.54421 -6.21245,-3.85894 -11.311553,-9.56187 -14.142066,-16.31064 -0.817917,-1.94869 -2.240702,-7.512 -3.222607,-10.95211 M 44.719333,176.46544 c 0.78638,-2.26282 1.876531,-3.3715 0.04138,-5.40739 m 51.420433,-57.08382 c -1.272777,-0.58634 -10.870671,-4.88507 -11.429116,-5.34808 -2.576883,-2.15461 -1.136642,-7.1397 -0.240626,-9.725895 m 49.661436,14.765665 c 4.04042,-1.80442 10.71127,-3.94969 16.32459,-7.8761 7.04778,-4.9351 12.84724,-12.065468 14.34221,-17.832369 m -1.45891,-11.669905 c -0.93653,1.526348 -7.12105,4.811733 -8.99758,5.588887 -10.62518,4.404589 -21.3819,-0.303737 -29.42199,-7.293245 -5.79368,-5.039805 -9.56535,-10.329718 -15.56029,-15.805751 -5.13986,-4.689616 -12.623864,-6.98497 -19.454113,-7.294434 -1.682257,-0.07685 -4.789698,-0.245389 -7.90757,0.137177 -3.144578,0.381583 -6.303203,1.316937 -8.021554,2.012633 -2.262748,0.917941 -4.871065,2.262811 -5.95301,2.958511 -1.917214,1.222721 -4.212579,3.871729 -5.693642,5.590072 l -2.195255,2.975921 -0.490953,3.313318 c -1.108719,2.358181 -1.408875,7.607397 -1.699712,10.456516 -0.277137,2.703688 0.05911,6.117056 0.927242,8.720721 1.458879,4.385958 5.757554,10.70662 9.307092,13.433628 2.117295,-0.44561 3.576243,-0.91793 5.657514,-1.612455 1.027283,-0.34554 1.64964,-0.51424 3.808905,-1.37745 m 12.606442,92.198225 c 2.267442,0 4.103289,1.83582 4.103289,4.10326 0,2.26744 -1.835847,4.10443 -4.103289,4.10443 -2.268625,0 -4.104402,-1.83699 -4.104402,-4.10443 0,-2.26744 1.835777,-4.10326 4.104402,-4.10326 z m 24.148394,-13.25679 c 2.26856,0 4.1044,1.83703 4.1044,4.10444 0,2.26278 -1.83584,4.10325 -4.1044,4.10325 -2.26744,0 -4.10329,-1.84047 -4.10329,-4.10325 0,-2.26741 1.83585,-4.10444 4.10329,-4.10444 z m 24.14954,-13.26023 c 2.26397,0 4.10444,1.84047 4.10444,4.10325 0,2.26863 -1.84047,4.10444 -4.10444,4.10444 -2.26748,0 -4.10325,-1.83581 -4.10325,-4.10444 0,-2.26278 1.83577,-4.10325 4.10325,-4.10325 z m -0.15821,-27.69901 c 2.26744,0 4.10329,1.83581 4.10329,4.10444 0,2.26274 -1.83585,4.10322 -4.10329,4.10322 -2.26393,0 -4.1044,-1.84048 -4.1044,-4.10322 0,-2.26863 1.84047,-4.10444 4.1044,-4.10444 z m -23.5178,-13.73258 c 2.26744,0 4.10322,1.83581 4.10322,4.10322 0,2.26747 -1.83578,4.10325 -4.10322,4.10325 -2.26393,0 -4.1044,-1.83578 -4.1044,-4.10325 0,-2.26741 1.84047,-4.10322 4.1044,-4.10322 z m -5.25853,17.64618 c -1.71251,0.20829 -2.79908,0.35837 -4.76289,2.68044 l -5.21194,9.0302 c -0.427076,0.87251 -0.481703,2.91313 -0.222544,3.76704 1.366974,2.24532 6.602214,12.12481 9.093034,12.09337 1.4682,-0.0209 12.22481,-0.0685 13.09277,-0.45489 1.34952,-0.60379 8.1122,-10.63797 8.0936,-12.32837 -0.0209,-1.81835 -2.02657,-5.71683 -3.80891,-8.60785 -1.24947,-2.02198 -2.24883,-4.13935 -2.91657,-5.23993 -0.43173,-0.71777 -1.18199,-0.94929 -1.73229,-0.96327 -2.93056,-0.0814 -10.87419,-0.0675 -11.62458,0.0243 z m -35.051614,0 c 4.435992,7.68882 5.994911,10.3832 7.10829,12.31091 0.849282,1.47637 0.653792,1.69509 0.253491,2.50828 -1.030795,1.78581 -4.707113,8.153 -6.907062,11.96541 l 0.0313,1.6276 c 1.258763,2.60831 3.494771,6.05308 6.648703,11.5198 0.545649,0.94936 0.354957,2.38146 0.118922,2.64441 -0.500202,0.55956 -4.798982,8.36239 -5.26316,9.12092 -1.444934,2.35003 -1.921908,3.06321 -1.408875,4.45811 l 6.922153,11.98401 c 0.0033,0.0104 0.394598,0.0769 1.485688,0.95864 4.762888,-0.34077 9.552551,-0.35485 14.2968,-0.7178 l 1.21342,-1.21808 c 1.967284,-2.65832 4.880384,-7.76674 6.367224,-10.339 0.44457,-0.21768 1.58103,-1.54032 2.39419,-1.68108 4.41741,-0.7725 12.76587,0.4863 15.19734,-1.21808 0.79457,-0.55841 1.44026,-1.48099 1.98588,-2.49895 1.34028,-2.50824 2.55366,-5.07703 3.89851,-7.58061 0.89111,-1.65899 1.07729,-2.52688 4.58145,-2.44426 3.07127,0.0769 6.15309,0.2222 9.15232,0.20342 1.92194,-0.0139 3.43548,-0.33625 4.42663,-1.64505 1.97195,-2.59903 4.50815,-6.59407 5.65759,-9.48394 1.92187,-3.29932 0.69103,-5.59468 -1.14131,-8.13902 -1.84513,-2.55828 -3.25282,-4.62679 -4.63958,-7.47128 -0.70383,-1.4449 -0.5131,-2.00449 1.24138,-4.52671 0.39411,-0.56307 5.27126,-7.54803 5.33872,-9.24308 l -0.33173,-1.56824 -6.47074,-12.40632 c -0.58637,-1.01797 -0.35033,-0.89466 -1.52289,-1.09012 -4.53485,-0.75039 -9.07089,-0.48633 -13.85592,-0.51769 -1.6543,-0.0104 -1.5496,0.19055 -2.47215,-1.29604 -1.49497,-2.4082 -3.74493,-7.37586 -5.27596,-10.04349 -1.06334,-1.84511 -0.82713,-1.73576 -2.70836,-1.74855 -3.895,-0.0264 -12.71576,-0.1593 -14.48763,0.003 l -6.858164,10.59841 c -0.636371,0.71315 -1.2355,2.68509 -4.121858,2.92593 -0.553751,0.0452 -4.276595,0.0139 -7.025705,0.064 -1.42275,0.0278 -2.73628,0 -4.143973,0.0243 -0.818996,0.0139 -1.818356,0.33625 -2.281386,1.13546 -1.772978,3.07249 -4.981572,8.62997 -6.417223,11.11613 z M 64.901407,97.927532 c 0,1.782323 -0.231237,4.598898 -1.944024,5.590078 l -2.672298,0.73178 -1.458843,-0.24539 c -2.913133,-0.22689 -3.640225,0.61777 -5.594715,-0.48627 -0.922548,-0.52236 -13.405722,-18.973638 -14.587707,-21.882086 -1.177326,-2.889848 -0.05459,-4.971143 0.727092,-7.538731 0.927243,-3.039934 4.436027,-17.460086 6.812829,-27.384931 0.427075,-1.663637 1.535656,-3.185363 3.266733,-4.486009 0.418834,-0.25363 0.800427,-0.544467 1.149438,-0.86323 l 55.934418,-7.962201 c 2.07192,3.563419 5.99373,5.894853 10.39714,5.730825 3.29935,-0.117392 10.9068,-1.926603 12.92409,-4.231228 l 0.17664,-2.536199 -0.15821,-1.581034 -2.30931,0.34077 -3.54481,0.504931 -1.06334,-1.441457 -3.17361,-7.056271 1.41816,-3.380856 0.70849,-1.694984 3.54484,-0.504861 2.14066,-0.277137 -0.1676,-1.595012 -0.81907,-2.109228 c -1.95797,-1.330845 -4.34405,-2.071916 -6.88839,-1.9765 -5.48535,0.199941 -14.611,4.508074 -15.5871,9.680512 l -53.839134,7.660894 c -0.586368,-0.739923 -1.354144,-1.349519 -2.217405,-1.848608 -1.494972,-0.864443 -4.780309,-0.595687 -8.611365,0.508408 -1.354178,0.390912 -2.004493,1.945171 -2.426839,3.227232 l -18.33729,2.612976 c -1.809038,0.253491 -3.080632,1.948649 -2.822377,3.757703 l 1.237726,5.575333 c 0.25975,1.809068 1.95456,3.081817 3.763597,2.827032 L 38.488804,43.08552 c 0.168647,0.449069 0.218371,0.880672 0.02086,1.290191 -6.198538,12.815848 -10.143543,20.518612 -14.09206,35.878799 l 0.477043,1.976595 0.622392,2.562939 c 1.849756,3.468048 3.208594,7.058246 4.61747,10.698473 2.422145,6.265993 5.135196,12.201573 8.025066,18.236063 l 0.727057,4.13585 m 158.866028,82.85042 c -0.99115,6.30324 -20.78157,13.10205 -26.45303,12.12481 -5.66798,-0.97259 -16.56546,-16.02913 -15.56957,-22.34979"
+       style="fill:none;stroke:#d947ad;stroke-width:10.4317;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path899" />
+    <path
+       d="m 192.76932,218.96976 c -5.05373,5.54002 -13.21142,7.72603 -15.07395,11.71992 -8.93948,19.18306 -14.72503,27.8945 -26.11805,36.04284 -9.6433,6.90351 -22.29978,14.53301 -41.5317,16.23734 -15.446239,1.37282 -25.32572,-0.57701 -39.714424,-6.04376 -6.480091,-2.46405 -22.834924,-13.20676 -26.88467,-18.57343 -5.571438,-7.38519 -10.615865,-12.18879 -13.206774,-21.36792 -2.821191,-10.00279 -5.421322,-19.68678 -5.638873,-30.29335 -0.109741,-5.44001 0.09994,-10.1715 0.667753,-16.12451 0.872532,-2.20812 2.295362,-4.31735 4.198684,-5.7844 0.19111,-0.14535 0.05925,-0.0685 0.178244,-1.75434 0.158145,-2.25464 0.853883,-2.82237 0.972575,-3.40408 0.172054,-0.84577 -0.391009,-9.71658 0.486314,-13.61509 1.831149,-8.12622 3.931106,-13.91058 5.712231,-21.07235 0.09531,-0.38629 0.14528,-0.67361 0.122087,-0.80975 -5.280629,-6.14384 -7.902911,-4.01716 -8.389172,-20.69545 l 0.777122,-0.83183 c 2.681631,-1.21338 9.757295,-4.34405 12.55638,-6.07518 m 35.32856,27.98405 -2.649035,2.08593 c -6.16248,1.59968 -13.342888,-1.74971 -15.073979,-8.26583 m -27.425636,49.82076 c 2.708316,4.40811 4.644244,8.17164 7.534107,12.40171 1.232012,1.79975 5.603995,8.5125 8.026137,8.5125 1.440293,0 2.767714,-0.0546 3.953208,-0.33625 1.809047,-0.42815 12.079399,-9.49789 13.710436,-18.87828 0.618984,-3.80424 3.563516,-8.6486 7.294462,-9.96554 0.994735,-0.35482 2.958475,-1.06331 4.135836,-0.48629 l 3.499465,0.73523 m 76.205202,48.32704 c -1.82187,5.47137 -8.10181,8.90686 -14.56907,11.03351 -7.91682,2.60365 -15.71034,4.04393 -23.82609,1.39493 -3.27727,-1.07266 -6.44052,-2.65836 -9.47581,-4.54421 -6.21248,-3.85894 -11.311549,-9.56187 -14.142062,-16.31064 -0.817918,-1.94869 -2.240702,-7.512 -3.222607,-10.95211 M 44.719483,176.46544 c 0.786397,-2.26282 1.876534,-3.3715 0.04121,-5.40739 M 96.181151,113.9743 c -1.272777,-0.58634 -10.870671,-4.88507 -11.429116,-5.34808 -2.576883,-2.15461 -1.136642,-7.1397 -0.240626,-9.725895 m 49.661431,14.765665 c 4.04042,-1.80442 10.71127,-3.94969 16.32459,-7.8761 7.04778,-4.9351 12.84724,-12.065469 14.34218,-17.83237 m -1.45887,-11.669904 c -0.93654,1.526347 -7.12105,4.811733 -8.99758,5.588887 -10.62522,4.404589 -21.3819,-0.303738 -29.42202,-7.293245 -5.79365,-5.039806 -9.56532,-10.329719 -15.5603,-15.805752 -5.13982,-4.689615 -12.623863,-6.984969 -19.454113,-7.294434 -1.682257,-0.07685 -4.789663,-0.245389 -7.907535,0.137178 -3.144613,0.381582 -6.303238,1.316937 -8.021554,2.012633 -2.262748,0.917941 -4.871065,2.262811 -5.95301,2.95851 -1.917249,1.222722 -4.212614,3.871729 -5.693642,5.590073 l -2.195266,2.975921 -0.490948,3.313318 c -1.108734,2.358181 -1.408869,7.607396 -1.69973,10.456516 -0.277172,2.703687 0.05925,6.117056 0.927242,8.72072 1.458889,4.385958 5.757578,10.706621 9.307081,13.433629 2.11733,-0.44561 3.576278,-0.91793 5.657549,-1.612455 1.027283,-0.34554 1.64964,-0.51424 3.808905,-1.37745 m 12.606442,92.198225 c 2.267442,0 4.103289,1.83581 4.103289,4.10326 0,2.26744 -1.835847,4.10443 -4.103289,4.10443 -2.268625,0 -4.104402,-1.83699 -4.104402,-4.10443 0,-2.26745 1.835777,-4.10326 4.104402,-4.10326 z M 114.44066,177.0331 c 2.26856,0 4.1044,1.83703 4.1044,4.10444 0,2.26278 -1.83584,4.10325 -4.1044,4.10325 -2.26744,0 -4.10329,-1.84047 -4.10329,-4.10325 0,-2.26741 1.83585,-4.10444 4.10329,-4.10444 z m 24.14953,-13.26023 c 2.26393,0 4.1044,1.84047 4.1044,4.10325 0,2.26863 -1.84047,4.10444 -4.1044,4.10444 -2.26748,0 -4.10329,-1.83581 -4.10329,-4.10444 0,-2.26278 1.83582,-4.10325 4.10329,-4.10325 z m -0.15821,-27.69901 c 2.26744,0 4.10329,1.83581 4.10329,4.10444 0,2.26274 -1.83585,4.10321 -4.10329,4.10321 -2.26393,0 -4.1044,-1.84047 -4.1044,-4.10321 0,-2.26863 1.84047,-4.10444 4.1044,-4.10444 z m -23.51779,-13.73258 c 2.26741,0 4.10321,1.83581 4.10321,4.10322 0,2.26747 -1.8358,4.10325 -4.10321,4.10325 -2.26396,0 -4.10444,-1.83578 -4.10444,-4.10325 0,-2.26741 1.84048,-4.10322 4.10444,-4.10322 z m -5.25854,17.64618 c -1.7125,0.20829 -2.79912,0.35837 -4.76289,2.68044 l -5.21197,9.0302 c -0.427075,0.87251 -0.481668,2.91313 -0.222544,3.76704 1.366984,2.24532 6.602254,12.12481 9.093034,12.09337 1.46823,-0.0209 12.22485,-0.0685 13.09277,-0.45489 1.34952,-0.60379 8.11223,-10.63798 8.09363,-12.32837 -0.0209,-1.81835 -2.02661,-5.71683 -3.8089,-8.60785 -1.24947,-2.02198 -2.24883,-4.13935 -2.9166,-5.23993 -0.43171,-0.71778 -1.18199,-0.94929 -1.7323,-0.96327 -2.93055,-0.0814 -10.87415,-0.0675 -11.62454,0.0243 z m -35.051609,0 c 4.435957,7.68882 5.994911,10.3832 7.10829,12.31091 0.849282,1.47637 0.653792,1.69509 0.253491,2.50828 -1.030795,1.78581 -4.707113,8.15299 -6.907062,11.96541 l 0.0313,1.6276 c 1.258763,2.60831 3.494771,6.05308 6.648703,11.5198 0.545649,0.94936 0.354922,2.38146 0.118922,2.64441 -0.500237,0.55956 -4.799017,8.36239 -5.26316,9.12092 -1.444934,2.35003 -1.921943,3.06321 -1.408875,4.45811 l 6.922153,11.98401 c 0.0033,0.0104 0.394598,0.0769 1.485653,0.95864 4.762888,-0.34077 9.552586,-0.35485 14.296835,-0.7178 l 1.213385,-1.21808 c 1.967324,-2.65832 4.880424,-7.76674 6.367224,-10.339 0.4446,-0.21768 1.58103,-1.54032 2.39422,-1.68108 4.41742,-0.7725 12.76584,0.4863 15.1973,-1.21808 0.79459,-0.55841 1.44031,-1.48099 1.98593,-2.49895 1.34023,-2.50825 2.55365,-5.07703 3.89848,-7.58061 0.89114,-1.65899 1.07732,-2.52688 4.58144,-2.44426 3.07131,0.0768 6.15313,0.2222 9.15236,0.20342 1.9219,-0.0139 3.43544,-0.33625 4.42663,-1.64505 1.97195,-2.59903 4.50815,-6.59407 5.65759,-9.48394 1.92187,-3.29932 0.69103,-5.59468 -1.14131,-8.13902 -1.84516,-2.55828 -3.25285,-4.62679 -4.63961,-7.47128 -0.7038,-1.4449 -0.51311,-2.00449 1.24141,-4.52671 0.39407,-0.56307 5.27122,-7.54803 5.33872,-9.24308 l -0.33173,-1.56824 -6.47077,-12.40632 c -0.58634,-1.01797 -0.3503,-0.89466 -1.52286,-1.09012 -4.53489,-0.75039 -9.07089,-0.48633 -13.85592,-0.51769 -1.6543,-0.0104 -1.54964,0.19055 -2.47219,-1.29604 -1.49493,-2.4082 -3.74492,-7.37586 -5.27592,-10.04349 -1.06334,-1.84511 -0.82717,-1.73576 -2.70839,-1.74855 -3.89497,-0.0264 -12.71577,-0.1593 -14.4876,0.003 l -6.858169,10.59841 c -0.636371,0.71315 -1.235535,2.68509 -4.121858,2.92593 -0.553751,0.0452 -4.276595,0.0139 -7.025705,0.064 -1.422785,0.0278 -2.73628,0 -4.143973,0.0243 -0.818996,0.0139 -1.818356,0.33625 -2.281386,1.13546 -1.773013,3.07249 -4.981607,8.62997 -6.417257,11.11613 z M 64.901407,97.927601 c 0,1.782324 -0.231237,4.598899 -1.944024,5.590079 l -2.672305,0.73177 -1.45884,-0.24538 c -2.913143,-0.22689 -3.640245,0.61777 -5.594722,-0.48627 -0.922555,-0.52237 -13.405731,-18.973638 -14.58772,-21.882086 -1.177305,-2.889849 -0.05459,-4.971144 0.727099,-7.538732 0.927242,-3.039933 4.43602,-17.460086 6.812828,-27.384931 0.427069,-1.663636 1.535649,-3.185362 3.266751,-4.486009 0.418827,-0.25363 0.80041,-0.544467 1.149432,-0.863229 L 106.5343,33.400612 c 2.07195,3.563419 5.99373,5.894853 10.39714,5.730824 3.29935,-0.117392 10.90683,-1.926603 12.92412,-4.231227 l 0.17664,-2.536199 -0.15821,-1.581034 -2.30934,0.34077 -3.54478,0.504931 -1.06334,-1.441458 -3.17361,-7.05627 1.41812,-3.380857 0.70853,-1.694983 3.5448,-0.504862 2.14067,-0.277136 -0.16726,-1.595013 -0.81906,-2.109227 c -1.95797,-1.330846 -4.34406,-2.071916 -6.88839,-1.976501 -5.48536,0.199942 -14.61101,4.508075 -15.58707,9.680512 l -53.83915,7.660895 c -0.586358,-0.739924 -1.354158,-1.34952 -2.217416,-1.848609 -1.494951,-0.864443 -4.780302,-0.595687 -8.611364,0.508408 -1.354158,0.390912 -2.0045,1.945172 -2.426833,3.227232 l -18.337283,2.612977 c -1.809044,0.253491 -3.080624,1.948648 -2.822373,3.757703 l 1.237733,5.575332 c 0.259646,1.809069 1.954528,3.081817 3.763576,2.827032 L 38.489145,43.08559 c 0.168473,0.449069 0.218441,0.880671 0.02052,1.29019 C 32.311116,57.191628 28.366101,64.894392 24.41758,80.254579 l 0.477044,1.976596 0.62242,2.562938 c 1.849745,3.468049 3.20859,7.058246 4.617459,10.698474 2.422142,6.265993 5.135196,12.201573 8.025055,18.236063 l 0.727051,4.13585 M 197.7527,200.71492 c -0.99116,6.30324 -20.78161,13.10205 -26.4531,12.12481 -5.66798,-0.97259 -16.56543,-16.02913 -15.56958,-22.34979"
+       style="fill:none;stroke:#f6d3e9;stroke-width:3.47725;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3779" />
+  </g>
+</svg>
diff --git a/hswaw/site/static/space.jpg b/hswaw/site/static/space.jpg
new file mode 100644
index 0000000..ef32f6f
--- /dev/null
+++ b/hswaw/site/static/space.jpg
Binary files differ
diff --git a/hswaw/site/templates/index.html b/hswaw/site/templates/index.html
index 058c162..64dfd46 100644
--- a/hswaw/site/templates/index.html
+++ b/hswaw/site/templates/index.html
@@ -2,80 +2,73 @@
 <meta charset="utf-8">
 <!-- https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-omission -->
 <title>Warszawski Hackerspace</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-<link rel="preconnect" href="https://fonts.gstatic.com">
+<meta name="viewport" content="width=device-width, initial-scale=1" />
 <link rel="stylesheet" href="/static/site/landing.css"/>
 <link rel="stylesheet" href="/static/leaflet/leaflet.css"/>
-<link href="https://fonts.googleapis.com/css2?family=Allerta&family=Lato&display=swap" rel="stylesheet">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&family=Noto+Sans&display=swap" rel="stylesheet">
 <style>
 </style>
+<div id="ledsFloater">
+<div id="ledsWrapper">
+    <div id="leds">
+    </div>
+</div>
+</div>
 <div id="page">
     <div class="top">
         <div class="logo">
-            <img src="/static/site/syrenka.png" />
+            <img src="/static/site/neon-syrenka.svg" />
+        </div>
+        <div class="type">
             <h1>Warszawski Hackerspace</h1>
         </div>
-        <div class="mapcontainer">
-            <h2>Gdzie jesteśmy?</h2>
-            <div id="map"></div>
-            <pre>Warszawski Hackerspace
-ul. Wolność 2A
-01-018 Warszawa 
-52°14'29.8"N 20°59'5.5"E</pre>
-        </div>
     </div>
-    <div class="covid">
-        <span>Na okres pandemii Hackerspace jest <b>zamknięty</b>, więcej informacji: <a href="">projekt covid-19</a></span>
+    <div class="quicklinks">
+        <ul>
+            <li><a href="https://wiki.hackerspace.pl/">Wiki</a></li>
+            <li><a href="https://profile.hackerspace.pl/">Konto</a></li>
+            <li><a href="https://wiki.hackerspace.pl/partners">Partnerzy</a></li>
+            <li><a href="https://wiki.hackerspace.pl/kontakt">Kontakt</a></li>
+            {{ if eq .AtError nil }}
+            {{ $count := len .AtStatus.Users }}
+            <li>
+                <a href="https://at.hackerspace.pl">Osób w spejsie: <b>{{ $count }}</b></a>
+            </li>
+            {{ end }}
+        </ul>
     </div>
     <div class="bottom">
         <div class="about">
-            <h2>Czym jest Hackerspace?</h2>
+            <h2>Czym jest Warszawski Hackerspace?</h2>
             <p>
-              Przestrzeń stworzona i utrzymywana przez grupę kreatywnych osób, które łączy fascynacja ogólno pojętym tworzeniem w duchu <a href="https://pl.wikipedia.org/wiki/Spo%C5%82eczno%C5%9B%C4%87_haker%C3%B3w">kultury hackerskiej</a>.
+              Przestrzeń stworzona i utrzymywana przez grupę kreatywnych osób, które łączy fascynacja ogólno pojętym tworzeniem w duchu <a href="https://pl.wikipedia.org/wiki/Spo%C5%82eczno%C5%9B%C4%87_haker%C3%B3w">kultury hackerskiej</a>. Razem utrzymujemy przestrzeń na ul. Wolność 2A, gdzie mamy między innymi:
+              <ul>
+                  <li><b>Warsztat ciężki</b>, ze sprzętem takim jak ploter laserowy, frezarka kolumnowa CNC, tokarka, spawarki i ramię robotyczne KUKA,</li>
+                  <li><b>Warsztat elektroniczny</b>, z oscyloskopami, stacjami lutowniczymi i masą części elektronicznych,</li>
+                  <li><b>Przestrzeń socjalną</b>, pełną stołów i kanap do hakowania nad projektami software'owymi,</li>
+                  <li><b>Serwerownię</b>, utrzymująca infrastrukturę spejsu i naszego mikro-ISP <a href="https://bgp.wtf">bgp.wtf</a>.</li>
+              </ul>
             </p>
             <p>
-              <b>Hackerspace nie zna barier.</b> Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy lub po prostu potrzebujesz miejsca i sprzętu - <a href="">zapraszamy</a>!
+              <b>Hackerspace nie zna barier.</b> Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy, lub po prostu potrzebujesz miejsca i sprzętu - <a href="https://wiki.hackerspace.pl/jak-dolaczyc">zapraszamy</a>! Utrzymujemy się w całosci z wolontariatu naszych członków, <a href="https://wiki.hackerspace.pl/finanse">darowizn i składek</a> oraz drobnej aktywności komercyjnej.
             </p>
-            <h3>Kto jest teraz w spejsie?</h3>
+            <img src="/static/site/space.jpg" />
+            <h2>Czy mogę odwiedzić spejs? Jak do was dołączyć?</h2>
             <p>
-                {{ if ne .AtError nil }}
-                <i>Ups, nie udało się załadować stanu checkinatora.</i>
-                {{ else }}
-                  {{ $count := len .AtStatus.Users }}
-                  {{ if gt $count 4 }}
-                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajduje się {{ $count }} osób:
-                  {{ else if gt $count 1 }}
-                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajdują się {{ $count }} osoby:
-                  {{ else if gt $count 0 }}
-                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajduje się jedna osoba:
-                  {{ else }}
-                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie nie ma nikogo.
-                  {{ end }}
-                <ul class="atlist">
-                    {{ range .AtStatus.Users }}
-                    <li>{{ .Login }}</li>
-                    {{ end }}
-                </ul>
-                {{ end }}
+              Nasze cotygodniowe otwarte spotkania są w tej chwili zawieszone z powodu pandemii. Mimo tego, <b>dalej jesteśmy otwarci na nowych członków</b> i zainteresowanych - tylko w mniejszej skali i po wcześniejszym umówieniu się. Więcej informacji znajdziesz na <a href="https://wiki.hackerspace.pl/jak-dolaczyc">wiki.hackerspace.pl/jak-dolaczyc</a>.
+            </p>
+            <h2>Gdzie jest Hackerspace?</h2>
+            <div id="map"></div>
+            <p>
+                Stowarzyszenie Warszawski Hackerspace, ul. Wolność 2A, 01-018 Warszawa.
+            </p>
+            <h2>Gdzie was znaleźć w Internecie?</h2>
+            <p>
+              Jeśli nalegasz, mamy rzadko aktualizowane konta na <a href="https://twitter.com/hackerspace.pl">Twitterze</a> i <a href="https://www.facebook.com/hackerspacepl">Facebooku</a>. Lepiej jednak kontaktować się z nami <a href="https://wiki.hackerspace.pl/kontakt">przez IRC, Matrixa lub mejlowo</a>.
             </p>
         </div>
-        <div class="blog">
-            <h2>Blog</h2>
-            <p>
-                Najnowsze wpisy z naszego <a href="https://blog.hackerspace.pl">bloga</a>:
-                <ul>
-                    {{ range .Entries }}
-                    <li><a href="{{ .Link.Href }}">{{ .Title }}</a> <i>{{ .UpdatedHuman }}, {{ .Author }}</i></li>
-                    {{ else }}
-                    <li><i>Ups, nie udało się załadować wpisów.</i></li>
-                    {{ end }}
-                </ul>
-            <p>
-        </div>
-    </div>
-
-    <div id="footer">
-        <span>&copy; 2021 <a href="https://cs.hackerspace.pl/hscloud/-/tree/hswaw/site">Autorzy Strony</a>. Ten utwór jest dostępny na <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">licencji Creative Commons Uznanie autorstwa 4.0 Międzynarodowe</a>.</span>
     </div>
 </div>
 
@@ -95,3 +88,4 @@
 }
 </script>
 <script src="/static/leaflet/leaflet.js" crossorigin="" onload="loadMap()"></script>
+<script src="/static/site/led.js" crossorigin="" type="module" ></script>
diff --git a/hswaw/site/views.go b/hswaw/site/views.go
index 09755b2..6109a55 100644
--- a/hswaw/site/views.go
+++ b/hswaw/site/views.go
@@ -5,9 +5,11 @@
 	"fmt"
 	"html/template"
 	"net/http"
+	"strings"
 
 	"github.com/golang/glog"
 
+	"code.hackerspace.pl/hscloud/hswaw/site/calendar"
 	"code.hackerspace.pl/hscloud/hswaw/site/templates"
 )
 
@@ -60,11 +62,47 @@
 
 	render(w, tmplIndex, map[string]interface{}{
 		"Entries":  s.getFeeds(),
+		"Events":   s.getEvents(),
 		"AtStatus": atStatus,
 		"AtError":  atError,
 	})
 }
 
+func (s *service) handleJSONEvents(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(s.getEvents())
+}
+
+// handleEvent is a fallback HTML-only event render view.
+// TODO(q3k): make this pretty by either making a template or redirecting to a
+// pretty viewer.
+func (s *service) handleEvent(w http.ResponseWriter, r *http.Request) {
+	parts := strings.Split(r.URL.Path, "/")
+	uid := parts[len(parts)-1]
+
+	events := s.getEvents()
+	var event *calendar.UpcomingEvent
+	for _, ev := range events {
+		if ev.UID == uid {
+			event = ev
+			break
+		}
+	}
+	if event == nil {
+		http.NotFound(w, r)
+		return
+	}
+
+	render(w, template.Must(template.New("event").Parse(`<!DOCTYPE html>
+	<meta charset="utf-8">
+	<title>Event details: {{ .Summary }}</title>
+	<body>
+	<i>this interface intentionally left ugly...</i><br/>
+	<b>summary:</b> {{ .Summary }}<br />
+	<b>date:</b> {{ .WarsawDate }}<br />
+	<pre>{{ .Description }}</pre>`)), event)
+}
+
 func (s *service) handleSpaceAPI(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 	w.Header().Set("Content-Type", "application/json")