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(' 110 + b = a.getpixel((x, y + 1)) > 110 + s += "█" if t and b else "▀" if t else "▄" if b else " " + lines.append(s.rstrip()) + return "\n".join(lines) + + +def b64png(im): + buf = io.BytesIO() + im.save(buf, "PNG", optimize=True) + return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode() + + +def main(): + for k in MARKS: + out = os.path.join(SITE, "static", "img", "logo", k) + os.makedirs(out, exist_ok=True) + tile(k, 192, radius=42).save(os.path.join(out, "tile.png"), optimize=True) + tile(k, 192, radius=96).save(os.path.join(out, "tile-round.png"), optimize=True) + lockup_png(k, 960, 540, True, 190).save(os.path.join(out, "splash.png"), optimize=True) + lockup_png(k, 600, 315, False, 130).save(os.path.join(out, "social.png"), optimize=True) + data = { + "display": svg(k, "display", uid=f"{k}d"), + "flat": svg(k, "flat"), + "min": svg(k, "min"), + "lockup_h": lockup(k), + "lockup_s": lockup(k, stacked=True), + "tab16": b64png(mark_png(k, "min", 16)), + "full16": b64png(mark_png(k, "flat", 16)), + "banner": banner(k), + "color": MARKS[k]["color"], + "ink": MARKS[k]["ink"], + } + with open(os.path.join(SITE, "data", "logo", f"{k}.json"), "w") as f: + json.dump(data, f, indent=1, ensure_ascii=False) + print(k, "ok") + + +if __name__ == "__main__": + sys.exit(main())