diff --git a/website/README.md b/website/README.md
index 0634614..13684dd 100644
--- a/website/README.md
+++ b/website/README.md
@@ -75,6 +75,10 @@ layouts/ Custom inline theme — no external theme or framework
index.html Home-only layout with editorial hero
assets/css/main.css Stylesheet (fingerprinted + minified on build)
assets/js/poll.js Vote script for /logo/ — plain fetch, no cookies, no storage
+data/logo/{m,r}.json Generated: mark SVGs, lockups, 16 px icons, console banner per proposal
+static/img/logo/ Generated: tiles, splash and social card per proposal
+tools/logo-assets.py Generator for the two lines above (cairosvg + Pillow + fontTools);
+ run on the dev box, commit the output. Font: tools/FONT-PROVENANCE.md
static/favicon.svg Gate mark in crimson
deploy.sh Manual rsync + remote Hugo build (over SSH, for off-CI pushes)
deploy-ci.sh Local rsync + Hugo build — runs on forge-runner-01 from CI
diff --git a/website/assets/css/main.css b/website/assets/css/main.css
index aeb5bb2..3d04992 100644
--- a/website/assets/css/main.css
+++ b/website/assets/css/main.css
@@ -458,50 +458,14 @@ main.container {
background: var(--card-bg);
transition: border-color 160ms, box-shadow 160ms;
}
-.poll-card.is-yours {
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-glow);
-}
+.is-yours { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
.poll-card h2 { margin: 0; font-size: 1.25rem; letter-spacing: -0.01em; }
.poll-desc { margin: 0; color: var(--fg-muted); font-size: 0.95rem; }
-.poll-art {
- display: grid;
- place-items: center;
- padding: 2.2rem 1rem;
- border-radius: 0.75rem;
-}
-.poll-art--m { background: var(--bg-subtle); color: var(--accent); }
-.poll-art--r { background: #151515; color: #af6428; }
+.poll-art { display: grid; place-items: center; padding: 2.2rem 1rem; border-radius: 0.75rem; }
+.poll-art--m { background: var(--bg-subtle); }
+.poll-art--r { background: #151515; }
+.poll-art .pm { height: 9rem; width: auto; }
.pm { display: block; }
-.pm--big { height: 9rem; width: auto; }
-.pm--head { height: 1.1em; width: auto; }
-.poll-ctx {
- display: flex;
- flex-wrap: wrap;
- gap: 0.6rem;
- align-items: center;
-}
-.poll-ctx-head, .poll-ctx-tab {
- display: inline-flex;
- align-items: center;
- gap: 0.45rem;
- padding: 0.35rem 0.7rem;
- border: 1px solid var(--border);
- border-radius: 0.5rem;
- background: var(--bg);
- font-size: 0.82rem;
- color: var(--fg-muted);
-}
-.poll-ctx-head { color: var(--accent); }
-.poll-ctx-head--r { color: #af6428; }
-.poll-ctx-name {
- font-weight: 600;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- font-size: 0.7rem;
- color: var(--fg);
-}
-.poll-ctx-tab img { image-rendering: pixelated; }
.poll-btn {
margin-top: auto;
padding: 0.85rem 1.2rem;
@@ -522,9 +486,76 @@ main.container {
.poll-bar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width 500ms ease; }
.poll--voted .poll-bar { display: block; }
.poll-count { margin: 0; min-height: 1.4em; font-size: 0.85rem; color: var(--fg-muted); font-variant-numeric: tabular-nums; }
-.poll-status { margin: 1.5rem 0 0; min-height: 1.5em; color: var(--fg-muted); }
+.poll-status { margin: 1.25rem 0 0; min-height: 1.5em; color: var(--fg-muted); }
.poll-note { margin-top: 1rem; max-width: var(--measure); font-size: 0.85rem; color: var(--fg-muted); }
.poll-note a { color: var(--accent); }
-@media (prefers-reduced-motion: reduce) {
- .poll-bar i, .poll-btn { transition: none; }
+
+/* showcase */
+.show { display: flex; flex-direction: column; gap: 3rem; margin-top: 4rem; }
+.show-intro { margin: 0; }
+.show-row { display: flex; flex-direction: column; gap: 0.6rem; }
+.show-h { margin: 0; font-size: 1.35rem; letter-spacing: -0.01em; }
+.show-p { margin: 0 0 0.6rem; max-width: var(--measure); color: var(--fg-muted); font-size: 0.95rem; }
+.show-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 1.25rem; }
+.show-cell {
+ display: flex; flex-direction: column; gap: 0.9rem;
+ padding: 1.1rem; border: 1px solid var(--card-border); border-radius: 0.9rem; background: var(--card-bg);
}
+.show-cell--m { color: var(--accent); }
+.show-cell--r { color: #af6428; }
+.show-tag { font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-muted); }
+.sizes { display: flex; align-items: flex-end; gap: 1.1rem; flex-wrap: wrap; }
+.sizes figure { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 0.35rem; }
+.sizes figcaption { font-family: var(--font-mono); font-size: 0.65rem; color: var(--fg-muted); }
+.sz { display: block; }
+.sz .pm { width: auto; }
+.sz-96 .pm { height: 96px; } .sz-48 .pm { height: 48px; } .sz-32 .pm { height: 32px; } .sz-24 .pm { height: 24px; }
+.px { image-rendering: pixelated; }
+.lock { display: grid; place-items: center; padding: 1rem; border-radius: 0.6rem; background: var(--bg-subtle); }
+.lock-h .pm { height: 3rem; width: auto; max-width: 100%; }
+.lock-s .pm { height: 7rem; width: auto; max-width: 100%; }
+.mock { border: 1px solid var(--border); border-radius: 0.7rem; overflow: hidden; background: var(--bg); color: var(--fg); font-size: 0.8rem; }
+.tabbar { background: var(--bg-subtle); padding: 0.55rem 0.65rem; }
+.tab { display: inline-flex; align-items: center; gap: 0.45rem; padding: 0.3rem 0.65rem; border: 1px solid var(--border); border-radius: 0.4rem; background: var(--bg); color: var(--fg-muted); }
+.urlbar { padding: 0.45rem 0.75rem; border-top: 1px solid var(--border); font-family: var(--font-mono); font-size: 0.72rem; color: var(--fg-muted); }
+.uihead { display: flex; align-items: center; gap: 0.5rem; padding: 0.8rem 0.9rem; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
+.uihead-mark { display: inline-flex; color: inherit; }
+.show-cell--m .uihead-mark { color: var(--accent); }
+.show-cell--r .uihead-mark { color: #af6428; }
+.uihead-mark .pm { height: 1.25rem; width: auto; }
+.uihead-name { font-weight: 600; letter-spacing: 0.13em; text-transform: uppercase; font-size: 0.72rem; color: var(--fg); }
+.uihead-nav { margin-left: auto; display: flex; gap: 0.7rem; color: var(--fg-muted); }
+.uibody { padding: 0.9rem; display: flex; flex-direction: column; gap: 0.45rem; }
+.uibody i { display: block; height: 0.45rem; border-radius: 3px; background: var(--bg-subtle); }
+.uibody i.s { width: 45%; }
+.tiles { display: flex; gap: 1rem; justify-content: center; padding: 1.2rem; border-radius: 0.6rem; background: var(--bg-subtle); }
+.tiles img { width: 6rem; height: 6rem; }
+.term {
+ margin: 0; padding: 0.9rem 1rem; border-radius: 0.6rem; overflow-x: auto;
+ background: #100f0c; color: #d8d2c2; font-family: var(--font-mono); font-size: 0.6rem; line-height: 1.3;
+}
+.term i, .term b { font-style: normal; font-weight: 400; color: inherit; }
+.show-cell--m .term i { color: #ff6b56; } .show-cell--m .term b { color: #ff6b56; }
+.show-cell--r .term i { color: #c9803f; } .show-cell--r .term b { color: #c9803f; }
+.shot { display: block; width: 100%; height: auto; border-radius: 0.6rem; }
+.grounds { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.6rem; }
+.ground { display: grid; place-items: center; padding: 1.2rem; border-radius: 0.6rem; border: 1px solid var(--border); }
+.ground--fg { background: var(--bg-subtle); color: var(--fg); }
+.ground .pm { height: 3.2rem; width: auto; }
+.poll-final { margin-top: 3.5rem; display: flex; flex-direction: column; gap: 1rem; }
+.poll-final-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1.25rem; }
+.poll-final-card {
+ display: flex; flex-direction: column; gap: 0.8rem; align-items: stretch;
+ padding: 1.1rem; border: 1px solid var(--card-border); border-radius: 0.9rem; background: var(--card-bg);
+}
+.poll-final-mark { display: grid; place-items: center; padding: 0.8rem; }
+.poll-final-mark .pm { height: 3.5rem; width: auto; }
+.home-poll {
+ display: inline-flex; align-items: center; gap: 0.6rem; margin-top: 1.4rem;
+ padding: 0.55rem 0.95rem; border: 1px solid var(--border); border-radius: 999px;
+ background: var(--card-bg); color: var(--fg); text-decoration: none; font-size: 0.92rem;
+}
+.home-poll:hover { border-color: var(--accent); }
+.home-poll .pm { height: 1.1rem; width: auto; color: var(--accent); }
+.home-poll .pm + .pm { color: #af6428; }
+@media (prefers-reduced-motion: reduce) { .poll-bar i, .poll-btn { transition: none; } }
diff --git a/website/assets/js/poll.js b/website/assets/js/poll.js
index 70cb18b..0a5f9bd 100644
--- a/website/assets/js/poll.js
+++ b/website/assets/js/poll.js
@@ -10,20 +10,22 @@
var busy = false;
function pct(n, total) { return total ? Math.round((n / total) * 100) : 0; }
+ function each(sel, fn) { Array.prototype.forEach.call(root.querySelectorAll(sel), fn); }
function render(d) {
var total = d.total || 0;
root.classList.toggle('poll--voted', !!d.yours);
Object.keys(d.counts).forEach(function (k) {
- var card = root.querySelector('[data-choice="' + k + '"]');
- if (!card) return;
- var n = d.counts[k], p = pct(n, total);
- card.classList.toggle('is-yours', d.yours === k);
- card.querySelector('.poll-bar i').style.width = p + '%';
- card.querySelector('[data-count]').textContent = d.yours ? p + ' % · ' + n : '';
- var b = card.querySelector('[data-vote]');
- b.disabled = d.yours === k;
- b.textContent = d.yours === k ? t.tVoted : (d.yours ? t.tChange : b.dataset.label);
+ var n = d.counts[k], p = pct(n, total), mine = d.yours === k;
+ each('[data-choice="' + k + '"]', function (card) {
+ card.classList.toggle('is-yours', mine);
+ var bar = card.querySelector('.poll-bar i'); if (bar) bar.style.width = p + '%';
+ var c = card.querySelector('[data-count]'); if (c) c.textContent = d.yours ? p + ' % · ' + n : '';
+ });
+ each('[data-vote="' + k + '"]', function (b) {
+ b.disabled = mine;
+ b.textContent = mine ? t.tVoted : (d.yours ? t.tChange : b.dataset.label);
+ });
});
status.textContent = d.yours ? t.tTotal.replace('{n}', total) : '';
}
@@ -35,7 +37,7 @@
.catch(function () { status.textContent = t.tError; });
}
- buttons.forEach(function (b) {
+ Array.prototype.forEach.call(buttons, function (b) {
b.dataset.label = b.textContent;
b.addEventListener('click', function () {
if (busy) return;
@@ -46,7 +48,7 @@
body: JSON.stringify({ choice: b.dataset.vote })
})
.then(function (r) { if (!r.ok) throw r; return r.json(); })
- .then(render)
+ .then(function (d) { render(d); status.scrollIntoView({ block: 'nearest' }); })
.catch(function () { status.textContent = t.tError; b.disabled = false; })
.then(function () { busy = false; });
});
diff --git a/website/content/_index.de.md b/website/content/_index.de.md
index 067ee9a..922180c 100644
--- a/website/content/_index.de.md
+++ b/website/content/_index.de.md
@@ -2,6 +2,8 @@
title: "Furtka"
description: "Offenes Heimserver-Betriebssystem — einfach genug für alle."
status: "26.20-alpha — in Arbeit"
+poll_link: "/de/logo/"
+poll_text: "Wir suchen ein Logo — hilf uns beim Aussuchen"
# features_today / features_next müssen index-parallel zu content/_index.md bleiben.
intro: |
**Furtka** ist ein offenes Heimserver-Betriebssystem.
@@ -19,11 +21,15 @@ features_today:
- "Apps mit einem Klick installieren und entfernen"
- "Eine installierte App mit einem Klick aktualisieren (holt das neueste Container-Image)"
- "Furtka selbst mit einem Klick aktualisieren — keine Neuinstallation mehr für neue Features"
+ - "Die Bedienseite fragt nach einem Passwort — niemand im WLAN kommt einfach so rein"
+ - "Neue Apps kommen aus einem Online-Katalog, ohne auf ein Furtka-Update zu warten"
+ - "Apps können auf anderen Apps aufbauen — was eine braucht, wird mit installiert"
+ - "Verschlüsselte Verbindung im Heimnetz, in den Einstellungen einschaltbar"
features_next_label: "Was als Nächstes kommt"
features_next:
- "Apps für Fotos, Dateien, Smarthome, Spiele-Streaming und Medien"
- "Einfachere Sprache im Einrichtungs-Assistenten"
- - "Sichere Verbindung im Heimnetz (ohne Warnmeldung im Browser)"
+ - "Den Server auch von unterwegs erreichen"
- "Mehrere Server zusammenschalten"
---
diff --git a/website/content/_index.md b/website/content/_index.md
index 295e2f2..ffb814e 100644
--- a/website/content/_index.md
+++ b/website/content/_index.md
@@ -2,6 +2,8 @@
title: "Furtka"
description: "Open-source home server OS — simple enough for everyone."
status: "26.20-alpha — work in progress"
+poll_link: "/logo/"
+poll_text: "We're picking a logo — help us choose"
# Keep features_today / features_next index-aligned with content/_index.de.md.
intro: |
**Furtka** is an open-source home server OS.
@@ -19,11 +21,15 @@ features_today:
- "Install and remove apps with one click"
- "Update an installed app with one click (pulls the newest container image)"
- "Update Furtka itself with one click — no reinstalling for new features"
+ - "The control page asks for a password — nobody on the Wi-Fi can just walk in"
+ - "New apps arrive from an online catalog, without waiting for a Furtka update"
+ - "Apps can build on other apps — what one needs gets installed alongside it"
+ - "Encrypted connection on your home network, switched on in settings"
features_next_label: "What's coming next"
features_next:
- "Apps for photos, files, smart home, game streaming and media"
- "Plainer language in the setup wizard"
- - "Secure connection on your home network (no browser warning)"
+ - "Reaching your server from outside the house"
- "Linking several servers together"
---
diff --git a/website/content/logo.de.md b/website/content/logo.de.md
index 9e57132..fe820ad 100644
--- a/website/content/logo.de.md
+++ b/website/content/logo.de.md
@@ -8,11 +8,32 @@ poll: logo
sitemap:
priority: 0.5
m:
+ short: "M"
title: "M — das f im Tor"
text: "Ein kleines f, aus einem Rundbogentor ausgestanzt. Der Stamm ist der Torpfosten, die zwei Balken sind die Querriegel."
r:
+ short: "R"
title: "R — Fenster, Rad und F"
text: "Ein Bogenfenster mit einem Speichenrad dahinter; in der oberen rechten Scheibe wird die Speiche zum F."
+show:
+ intro: "Noch unentschieden? Hier ist jedes Zeichen überall dort, wo es funktionieren muss."
+ sizes: "Groß und klein"
+ sizes_text: "Vom Poster bis zum 16-Pixel-Symbol im Browser-Tab. Unter 32 Pixeln wechseln beide auf eine vereinfachte Zeichnung — M verliert nichts, R behält Fenster, Kreuz und Nabe."
+ name: "Mit dem Namen"
+ name_text: "Nebeneinander und gestapelt. Die Wortmarke ist bei beiden dieselbe, also entscheidet nur das Zeichen."
+ browser: "Im Browser"
+ browser_text: "Das Tab-Symbol in echter Größe und die Kopfzeile von furtka.org."
+ nav1: "Doku"
+ nav2: "Blog"
+ phone: "Auf dem Handy"
+ phone_text: "Startbildschirm-Symbol — eckig wie auf Android, rund wie manche Launcher es beschneiden."
+ box: "Auf dem Server"
+ box_text: "Der Text-Willkommensbildschirm, den der Server am Monitor zeigt, und der Startbildschirm beim Booten."
+ shared: "Geteilt"
+ shared_text: "Die Vorschaukarte, die erscheint, wenn jemand einen Link zu furtka.org postet."
+ colour: "In Farbe"
+ colour_text: "Auf hell, auf dunkel, auf der eigenen Farbe und in normaler Textfarbe — jedes Zeichen ist eine Form und nimmt die Farbe an, die es umgibt."
+ final: "Genug gesehen?"
labels:
vote: "Dieses hier"
voted: "Deine Wahl"
@@ -26,5 +47,6 @@ labels:
*Furtka* ist polnisch für die kleine Gartenpforte — die Tür im Zaun, durch
die man hineinkommt, ohne das große Tor aufzumachen. Beide Zeichen spielen
damit: das eine zeichnet den Buchstaben ins Tor, das andere schaut durchs
-Tor auf ein Rad. Unter jedem Zeichen siehst du, wie es in der Kopfzeile
-der Seite sitzt und als echtes 16-Pixel-Symbol im Browser-Tab.
+Tor auf ein Rad. Stimm direkt hier ab — oder scroll erst weiter und sieh
+beide Zeichen überall, wo sie funktionieren müssen, vom Poster bis zum
+winzigen Symbol im Browser-Tab.
diff --git a/website/content/logo.md b/website/content/logo.md
index 75c954f..7091e93 100644
--- a/website/content/logo.md
+++ b/website/content/logo.md
@@ -9,11 +9,32 @@ poll: logo
sitemap:
priority: 0.5
m:
+ short: "M"
title: "M — the f in the gate"
text: "A lowercase f cut out of an arched gate. The stem is the gate post, the two rails are the crossbars."
r:
+ short: "R"
title: "R — window, wheel and F"
text: "An arched window with a wheel of spokes behind it; in the top-right pane the spoke becomes an F."
+show:
+ intro: "Not sure yet? Here is each mark everywhere it would have to work."
+ sizes: "Big and small"
+ sizes_text: "From a poster to the 16-pixel icon in a browser tab. Below 32 pixels both marks switch to a simplified drawing — M drops nothing, R keeps the window, the cross and the hub."
+ name: "With the name"
+ name_text: "Side by side and stacked. The wordmark is the same for both, so only the mark decides."
+ browser: "In the browser"
+ browser_text: "The tab icon at its real size, and the site header of furtka.org."
+ nav1: "Docs"
+ nav2: "Blog"
+ phone: "On the phone"
+ phone_text: "Home-screen icon — square as on Android, round as some launchers cut it."
+ box: "On the box"
+ box_text: "The text-mode welcome screen your server shows on a monitor, and the boot splash."
+ shared: "Shared"
+ shared_text: "The preview card that appears when someone posts a link to furtka.org."
+ colour: "In colour"
+ colour_text: "On light, on dark, on its own colour, and in plain text colour — each mark is one shape and takes whatever colour is around it."
+ final: "Seen enough?"
labels:
vote: "This one"
voted: "Your pick"
@@ -27,5 +48,5 @@ labels:
*Furtka* is Polish for a small garden gate — the little door in the fence
that lets you in without opening the whole thing. Both marks play with that:
one draws the letter into the gate, the other looks through the gate at a
-wheel. Below each mark you see how it sits in the site header and as a
-real 16-pixel browser-tab icon.
+wheel. Vote right here — or scroll on first and see both marks everywhere
+they would have to work, from a poster to the tiny icon in a browser tab.
diff --git a/website/data/logo/m.json b/website/data/logo/m.json
new file mode 100644
index 0000000..ea69beb
--- /dev/null
+++ b/website/data/logo/m.json
@@ -0,0 +1,12 @@
+{
+ "display": "",
+ "flat": "",
+ "min": "",
+ "lockup_h": "",
+ "lockup_s": "",
+ "tab16": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABJklEQVR42sWRvUoDURCFv9msf1hoZyFCGmEjWks2iIkSi9gIIZ2FQWx9BX0Gn0GsrUwjCSmMaB1ICBEsxMJGK8F13bHYmFzWJQQi+HX3zsyZc88VIlyvry7YCf8YZQ9huXfdUeRyEu8s03h4MfvFPFTd1K5FcA4yTzyvIrK/edO6+iVQTa9sWxJUgAmG4wXKztZtu94XqGWT0+JNdUCWGI3H9znfKVS6HzaAeDMlUGNY7hUOc41W05yquc6pwAmQnH2zi8CFFZaCvNHn69dnMTocRYU8gN3buGjUnnN33SeAetpxEdkYDGkG/QkvdBwKKJbxH4Gx5Qj0YLDWcKCSMBzEk220y0A5JoM+FmMy1EE9kyqgrMVlMJKAqpaA2Az+7An/LzA232GMWfyHzvuOAAAAAElFTkSuQmCC",
+ "full16": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABJUlEQVR42sWRz0oCYRTFf3dmkDbSbqCwTQQzPUGkgRriE6TQrnqGwKB1u54jwjbtIxzB0hdIC7fppjb92SQ1t0WMfZlOgUFnd7977uGc8wlj0Mh4yYFK0UaWABDt2ujZ2sXN0yhXzKFawnb7fgVlD0iOcB9BDrKXnUOB8JtAtYTt9vxjYIM4qFSzzc5mJGJF727fr/x4/BGnXE8v735x0Mh4yVeVnmH7NlRr6z7VDsonvEXkYNWvIeSAB03YqXxw9ewADFSKlpFZQ9leb7XPY3zM8hIWgFMHwBI89HN7t9Cp0YJ62ttXZMc4nDfK8AEcAFUS5ndEtlWlgLA4vgtmhgKTkGte583Z6GAIiykR6yBI+0fAyvgOfiGAMjexg7+K8P8CU+Md2cpUHzmzh4cAAAAASUVORK5CYII=",
+ "banner": " ▄▄██████▄▄\n ▄████████████▄\n ██████▀▀▀▀▀▀████\n ████▀ ████\n █████ ██████████\n █████ █████\n █████ ▄▄▄▄▄█████\n █████ ██████████\n █████ ██████████\n ▀▀▀▀ ▀▀▀▀▀▀▀▀▀",
+ "color": "#c03a28",
+ "ink": "#0d0d0f"
+}
\ No newline at end of file
diff --git a/website/data/logo/r.json b/website/data/logo/r.json
new file mode 100644
index 0000000..fc38af4
--- /dev/null
+++ b/website/data/logo/r.json
@@ -0,0 +1,12 @@
+{
+ "display": "",
+ "flat": "",
+ "min": "",
+ "lockup_h": "",
+ "lockup_s": "",
+ "tab16": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAQCAYAAADJViUEAAAB4klEQVR42o2TPWhTYRSGn/Pdm7Qd/NnqpoPYqIvVguAP7SwUmlRrhw5qrx1dHF0c7OoiFElTOyhOpjFrJ6uIgihSKKYloiBCwSJ00bTJva9DbtubEKHveM77nPN+Bz4joYUgk3PGLYl+oCfR+gt8FJrLFVbLO0UDmL9xrPuQ3/3cIGuwLrQko8dhQxF6ZXI10CDQK3jR3dOYuPKouuUADqe68gYjwP3tTe9otrA6jngicRBRk6Jvkp4iKxtc3aqlZgDs5dSp84qi94iH2bnK3Z1IxckTgTM3S6veGXwQ3AEGXKToOlCv400nXV7kfgBImjazXD3tHcgWKhe2w8YDoCFj3JnUB1Ydm1v5nYQjz44DmNk9SQt+PZwCGJuv/gK+msg4sBSm7SQoMCPqjW/6zAsZ2qzXZtg7cw3o8umgUtCXNyyIR02EHn9uFr4vtfusFGQWDU5j9raZl1RkGrGkCYSsjKPenKeLghU/jplGOtd0mmdtGwSG0Y8UxqU0gB+P/pmdrZzZjT158g2mSwn4da7wZXC3fzvzGYHr9GbfC7OCUkwWUy4a7eTrCA/n1zYs4nEM54fzaxv7hvcrKwWZReAsUGw5kunI3sew9TZuFPjkA8vAAHCtZaoMQWjY5f8sXv4HrfW+WZX2x04AAAAASUVORK5CYII=",
+ "full16": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAQCAYAAADJViUEAAAChUlEQVR42nWTT4jUZRjHP993fr/ZSSSKoFu4UDC/TQNLErrUVQndSaigw2o0oyRG7KkgFAKxS9TFIpp1W4I6eHCmdutSHcwgoaK10FndFkwIRNSD5u7On9/77TA7y4T1PT087/u8z/f7fd5HDKH58paHnHT3AtuwNg7yhtvgX9LEM7s+uvTXIB8GQaNaftWF3gLWdqMzwrPAVuFZxA9BeqrXC5ca1Wz/oEYAjVq2D/OeCOOVqQtnAOZeeuz+7obuTQhvS14BIHrE8qTFoT31i5/p5MHNG9NOfkVmb+XEwuzg1VMT2QMqcl1SE+j2+XseqWW7vqFd3JQUO/HNiEuBpDWsX6GUwyo2p2X+7uRh7oWZ81cBmtXs2HKpsyNER7CWovL5Rq38+qA4KRaSNbvGLb84Enpj6wbKP2E9mgQFsH9Emoxm5lQ1e6aU9w6s5Ms7giDEMLV7+sLnAq+zilpFjKy7Xam3vk1D3CpZ7UJ6OYT4LvBHVP5Oszr2Af+BZG2Oo83a2Bu5QdG/Wq6EGHaOn2h9//VrWx7uLPcWm7XsKqgN4OhRwbU1XdwLbOvrCSWItNPiEkAnp2Mh9c/bQ/f7nQW/Veqt/QY1atmk7GfTbvurL2rln2Ontx3II5x+rt56X+DmK9nHAGpWx45iPyglhyPdadDmQs6hPPFOrIOI47a+FP4Q/Gck31dwcgTpWuhTZTTSOy+FO2maPr77k4U5uve8BUC3dHjPVOubEP0EaClQ+B14BCCJuCx4GnmiUl84yf9gfPribeBAo1b+zvCp8Y2koLAYo88B9/3706+UjK4rWZloVLNVhlYM61whaDGB/CzSk8Dzw53c35l5o1130ZBvQTz7D8VtHXvXpMGjAAAAAElFTkSuQmCC",
+ "banner": " ▄▄████████▄▄\n ▄██▀ ██▄▄▄███▄\n▄█▀ ██▀▀▀▀ ▀█▄\n██ ████▀ ██\n██ ▄████▄ ██\n██████████████████\n██ ██████ ██\n██ ██ ██\n██ ██ ██\n██████████████████",
+ "color": "#af6428",
+ "ink": "#151515"
+}
\ No newline at end of file
diff --git a/website/layouts/index.html b/website/layouts/index.html
index 9b88d3b..5975241 100644
--- a/website/layouts/index.html
+++ b/website/layouts/index.html
@@ -12,6 +12,9 @@
→
+ {{ with .Params.poll_link }}
+ {{ partial "mark-m.html" (dict "class" "" "id" "hm") }}{{ partial "mark-r.html" (dict "class" "" "id" "hr") }}{{ $.Params.poll_text }} →
+ {{ end }}
{{ with .Params.intro }}
diff --git a/website/layouts/poll/single.html b/website/layouts/poll/single.html
index 9b4bcbc..e62b99b 100644
--- a/website/layouts/poll/single.html
+++ b/website/layouts/poll/single.html
@@ -1,5 +1,8 @@
{{ define "main" }}
{{- $p := .Params -}}
+{{- $M := hugo.Data.logo.m -}}
+{{- $R := hugo.Data.logo.r -}}
+{{- $s := $p.show -}}
{{ .Content }}
-
-
- {{ partial "mark-m.html" (dict "class" "pm--big" "id" "m") }}
-
-
{{ partial "mark-m.html" (dict "class" "pm--head" "id" "mh") }}Furtka
-
Furtka
-
- {{ $p.m.title }}
- {{ $p.m.text }}
-
+ {{/* ── Quick vote ─────────────────────────────────────────── */}}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+ {{ $c := index $p $k }}
+
+ {{ $d.display | safeHTML }}
+ {{ $c.title }}
+ {{ $c.text }}
+
-
-
-
-
- {{ partial "mark-r.html" (dict "class" "pm--big" "id" "r") }}
-
-
{{ partial "mark-r.html" (dict "class" "pm--head" "id" "rh") }}Furtka
-
Furtka
-
- {{ $p.r.title }}
- {{ $p.r.text }}
-
-
-
+
+ {{ end }}
-
{{ $p.labels.nojs }}
+
+ {{/* ── Showcase ───────────────────────────────────────────── */}}
+
+
{{ $s.intro }}
+
+
+ {{ $s.sizes }}
+ {{ $s.sizes_text }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ index $p $k "short" }}
+
+
{{ $d.flat | safeHTML }}96
+
{{ $d.flat | safeHTML }}48
+
{{ $d.min | safeHTML }}32
+
{{ $d.min | safeHTML }}24
+
16
+
+
+ {{ end }}
+
+
+
+
+ {{ $s.name }}
+ {{ $s.name_text }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ index $p $k "short" }}
+
{{ $d.lockup_h | safeHTML }}
+
{{ $d.lockup_s | safeHTML }}
+
+ {{ end }}
+
+
+
+
+ {{ $s.browser }}
+ {{ $s.browser_text }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ index $p $k "short" }}
+
+
Furtka
+
https://furtka.org
+
{{ $d.flat | safeHTML }}Furtka{{ $s.nav1 }}{{ $s.nav2 }}
+
+
+
+ {{ end }}
+
+
+
+
+ {{ $s.phone }}
+ {{ $s.phone_text }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ index $p $k "short" }}
+
+
+ {{ end }}
+
+
+
+
+ {{ $s.box }}
+ {{ $s.box_text }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ index $p $k "short" }}
+
{{ $d.banner }}
+
+ Furtka is ready. Open http://furtka.local
+

+
+ {{ end }}
+
+
+
+
+ {{ $s.shared }}
+ {{ $s.shared_text }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ index $p $k "short" }}
+

+
+ {{ end }}
+
+
+
+
+ {{ $s.colour }}
+ {{ $s.colour_text }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ index $p $k "short" }}
+
+ {{ $d.flat | safeHTML }}
+ {{ $d.flat | safeHTML }}
+ {{ $d.flat | safeHTML }}
+ {{ $d.flat | safeHTML }}
+
+
+ {{ end }}
+
+
+
+
+ {{/* ── Vote again at the end ─────────────────────────────── */}}
+
+ {{ $s.final }}
+
+ {{ range $k, $d := (dict "m" $M "r" $R) }}
+
+
{{ $d.flat | safeHTML }}
+
+
+
+
+ {{ end }}
+
+
{{ $p.labels.note | safeHTML }}
{{ $js := resources.Get "js/poll.js" | minify | fingerprint }}
diff --git a/website/static/img/logo/m/social.png b/website/static/img/logo/m/social.png
new file mode 100644
index 0000000..0a5477e
Binary files /dev/null and b/website/static/img/logo/m/social.png differ
diff --git a/website/static/img/logo/m/splash.png b/website/static/img/logo/m/splash.png
new file mode 100644
index 0000000..7d43290
Binary files /dev/null and b/website/static/img/logo/m/splash.png differ
diff --git a/website/static/img/logo/m/tile-round.png b/website/static/img/logo/m/tile-round.png
new file mode 100644
index 0000000..433998e
Binary files /dev/null and b/website/static/img/logo/m/tile-round.png differ
diff --git a/website/static/img/logo/m/tile.png b/website/static/img/logo/m/tile.png
new file mode 100644
index 0000000..d4bb0b9
Binary files /dev/null and b/website/static/img/logo/m/tile.png differ
diff --git a/website/static/img/logo/r/social.png b/website/static/img/logo/r/social.png
new file mode 100644
index 0000000..bc25571
Binary files /dev/null and b/website/static/img/logo/r/social.png differ
diff --git a/website/static/img/logo/r/splash.png b/website/static/img/logo/r/splash.png
new file mode 100644
index 0000000..acf2967
Binary files /dev/null and b/website/static/img/logo/r/splash.png differ
diff --git a/website/static/img/logo/r/tile-round.png b/website/static/img/logo/r/tile-round.png
new file mode 100644
index 0000000..3a74a9a
Binary files /dev/null and b/website/static/img/logo/r/tile-round.png differ
diff --git a/website/static/img/logo/r/tile.png b/website/static/img/logo/r/tile.png
new file mode 100644
index 0000000..cc56a64
Binary files /dev/null and b/website/static/img/logo/r/tile.png differ
diff --git a/website/tools/FONT-PROVENANCE.md b/website/tools/FONT-PROVENANCE.md
new file mode 100644
index 0000000..8de91b9
--- /dev/null
+++ b/website/tools/FONT-PROVENANCE.md
@@ -0,0 +1,9 @@
+# Build-time font
+
+`FamiljenGrotesk-SemiBold.ttf` — Familjen Grotesk, weight 600, fetched from
+Google Fonts (fonts.gstatic.com, family `Familjen+Grotesk:wght@600`) on
+2026-08-26. Licence: SIL Open Font License 1.1.
+
+It is used **only** by `tools/logo-assets.py` to outline the wordmark into
+SVG paths and to render the splash / social-card PNGs. The site itself ships
+no webfonts.
diff --git a/website/tools/FamiljenGrotesk-SemiBold.ttf b/website/tools/FamiljenGrotesk-SemiBold.ttf
new file mode 100644
index 0000000..946a979
Binary files /dev/null and b/website/tools/FamiljenGrotesk-SemiBold.ttf differ
diff --git a/website/tools/logo-assets.py b/website/tools/logo-assets.py
new file mode 100644
index 0000000..f9a865e
--- /dev/null
+++ b/website/tools/logo-assets.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+"""Generate the logo-poll showcase assets for /logo/ from one geometry source.
+
+ python3 website/tools/logo-assets.py
+
+Writes website/static/img/logo/{m,r}/*.png and website/data/logo/{m,r}.json.
+Needs: cairosvg, Pillow, fontTools (pip). Run it on the dev box and commit
+the output — the site build itself needs nothing beyond Hugo.
+
+Mark M: lowercase f knocked out of a gate, 64-unit grid (even-odd fill).
+Mark R: window, wheel and F, 104x114 grid, stroke 8 (from Robert's sketch).
+"""
+import base64
+import io
+import json
+import os
+import sys
+
+import cairosvg
+from fontTools.pens.svgPathPen import SVGPathPen
+from fontTools.ttLib import TTFont
+from PIL import Image, ImageDraw
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+SITE = os.path.dirname(HERE)
+FONT = os.path.join(HERE, "FamiljenGrotesk-SemiBold.ttf")
+XMLNS = 'xmlns="http://www.w3.org/2000/svg"'
+
+# ---------------------------------------------------------------- geometry
+M_FLAT = (
+ "M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6"
+ "H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z"
+)
+M_MIN = (
+ "M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32"
+ "a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"
+)
+R_FRAME = "M6 108V42A36 36 0 0 1 42 6H62A36 36 0 0 1 98 42V108Z"
+R_OUTER = "M2 112V42A40 40 0 0 1 42 2H62A40 40 0 0 1 102 42V112Z"
+R_HOLE = "M63 65a11 11 0 1 0-22 0a11 11 0 1 0 22 0Z"
+R_SPOKES = "M52 8V106 M8 65H96 M52 65L4 31.1 M52 65L4 98.9 M52 65L100 98.9"
+R_ARMS = "M52 22H79M52 37H69"
+
+MARKS = {
+ "m": {
+ "vb": (0, 0, 64, 64),
+ "box": (6, 2, 52, 58), # x, y, w, h of the drawn mark inside the viewBox
+ "color": "#c03a28",
+ "ink": "#0d0d0f",
+ "grad": ("#d95a45", "#c03a28", "#8a2618"),
+ },
+ "r": {
+ "vb": (0, 0, 104, 114),
+ "box": (2, 2, 100, 110),
+ "color": "#af6428",
+ "ink": "#151515",
+ "grad": ("#c9803f", "#af6428", "#9a561f"),
+ },
+}
+
+
+def body(k, tier, color="currentColor", uid=""):
+ """SVG inner markup for mark k at tier display | flat | min."""
+ if k == "m":
+ if tier == "display":
+ g = MARKS["m"]["grad"]
+ return (
+ f''
+ f''
+ f''
+ f''
+ f''
+ f''
+ )
+ path = M_MIN if tier == "min" else M_FLAT
+ return f''
+ if tier == "min":
+ return (
+ f''
+ f''
+ f''
+ f''
+ )
+ defs = (
+ f''
+ )
+ if tier == "display":
+ g = MARKS["r"]["grad"]
+ defs += (
+ f''
+ f''
+ f''
+ )
+ color = f"url(#g{uid})"
+ return (
+ f"{defs}"
+ f''
+ f''
+ f''
+ )
+
+
+def svg(k, tier, color="currentColor", uid="", cls="pm", standalone=False):
+ x, y, w, h = MARKS[k]["vb"]
+ ns = f" {XMLNS}" if standalone else ""
+ return (
+ f'"
+ )
+
+
+# ---------------------------------------------------------------- raster
+def raster(svgtxt, w, h):
+ png = cairosvg.svg2png(bytestring=svgtxt.encode(), output_width=w, output_height=h)
+ return Image.open(io.BytesIO(png)).convert("RGBA")
+
+
+def mark_png(k, tier, height, color=None):
+ _, _, w, h = MARKS[k]["vb"]
+ return raster(svg(k, tier, color or MARKS[k]["color"], standalone=True), round(height * w / h), height)
+
+
+# ---------------------------------------------------------------- wordmark
+_font = TTFont(FONT)
+_upm = _font["head"].unitsPerEm
+_cap = _font["OS/2"].sCapHeight or int(_upm * 0.7)
+_glyphs = _font.getGlyphSet()
+_cmap = _font.getBestCmap()
+
+
+def wordmark(text, size, x0=0.0, baseline=0.0, tracking=-0.028):
+ """Outline text as one SVG path; returns (path_d, advance_width)."""
+ s = size / _upm
+ parts, x = [], x0
+ for ch in text:
+ name = _cmap[ord(ch)]
+ pen = SVGPathPen(_glyphs)
+ g = _glyphs[name]
+ g.draw(pen)
+ d = pen.getCommands()
+ if d:
+ parts.append(f'')
+ x += g.width * s + tracking * size
+ return "".join(parts), x - x0 - tracking * size
+
+
+def lockup(k, stacked=False):
+ """Mark + 'furtka', outlined, currentColor. Returns an inline SVG string."""
+ bx, by, bw, bh = MARKS[k]["box"]
+ H = 58.0 # mark height in lockup units
+ sc = H / bh
+ mw = bw * sc
+ size = H * 0.72 * _upm / _cap # cap height = 72 % of the mark height
+ if stacked:
+ paths, tw = wordmark("furtka", size)
+ gap = 12
+ total_w = max(mw, tw)
+ total_h = H + gap + size * _cap / _upm + size * 0.22
+ mx = (total_w - mw) / 2
+ tx = (total_w - tw) / 2
+ base = H + gap + size * _cap / _upm
+ paths, _ = wordmark("furtka", size, x0=tx, baseline=base)
+ inner = (
+ f'{body(k, "flat")}'
+ f'{paths}'
+ )
+ return f''
+ gap = H * 0.28
+ paths, tw = wordmark("furtka", size, x0=mw + gap, baseline=H)
+ total_w = mw + gap + tw
+ inner = (
+ f'{body(k, "flat")}'
+ f'{paths}'
+ )
+ return f''
+
+
+def lockup_png(k, W, Hpx, stacked, mark_h):
+ """Rasterise the lockup on the mark's ink ground (splash / social card)."""
+ ink = MARKS[k]["ink"]
+ col = MARKS[k]["color"]
+ inner = lockup(k, stacked).replace("currentColor", col)
+ inner = inner.replace('