feat(brand): Mark M is the Furtka logo — website, installer, box UI, console, boot splash
All checks were successful
CI / lint (pull_request) Successful in 30s
CI / validate-json (pull_request) Successful in 24s
CI / markdown-links (pull_request) Successful in 16s
CI / test (pull_request) Successful in 1m47s
CI / lint (push) Successful in 28s
CI / test (push) Successful in 1m56s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 21s
Deploy site / deploy (push) Successful in 5s
Build ISO / build-iso (push) Successful in 20m27s

The logo vote on furtka.org closed with M 29 · R 13 (42 votes).

- website/tools/brand-assets.py generates every brand file from the
  logo-assets.py geometry: favicons (small-size tier, accent with a
  dark-mode variant) for website / webinstaller / on-box UI, 180 px
  apple-touch-icon, 1200x630 default share card, 640x480 syslinux splash,
  and docs/brand/ (mark, small mark, lockups, 20-column console art)
- website: header mark, favicon, touch icon, default og:image; /logo/ now
  shows the final result (static counts, winner highlighted, no voting
  UI or poll.js); homepage link reads 'meet our new logo'
- webinstaller: M in the header + tab icon
- on-box UI: .brand mark via CSS mask in brand red (not the UI's green),
  favicon on every page, brand line on the login page
- console: M art above the text of /etc/issue on the installed box
  (furtka-welcome) and the live ISO (furtka-update-issue)
- ISO: build.sh swaps releng's Arch splash for the Furtka one
- README shows the lockup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
This commit is contained in:
Daniel Maksymilian Syrnicki 2026-09-28 15:08:49 +02:00
parent 0cad2e05c8
commit 91aa33b891
33 changed files with 257 additions and 40 deletions

View file

@ -7,6 +7,18 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
## [Unreleased] ## [Unreleased]
### Added
- **New logo: Mark M** (a lowercase *f* cut out of an arched gate), winner of
the furtka.org vote (29 of 42 votes). It is now in the website header,
favicon and share card, the installer header and tab icon, the on-box UI
(every page and the login), the console welcome screens of the live ISO
and the installed box, and the BIOS boot-menu splash. Brand files in
`docs/brand/`, generated by `website/tools/brand-assets.py`.
- **furtka.org/download/** (and `/de/download/`): ISO download with size,
date and checksum, requirements and step-by-step install guide. Releases
now publish `furtka-<ver>.iso.sha256` and refresh the site.
### Security ### Security
- **App names are validated everywhere.** `POST /api/apps/remove` with - **App names are validated everywhere.** `POST /api/apps/remove` with

View file

@ -1,3 +1,5 @@
<p><img src="docs/brand/furtka-lockup.svg" alt="Furtka" height="56"></p>
# Furtka # Furtka
**Open-source home server OS — simple enough for everyone.** · [furtka.org](https://furtka.org) **Open-source home server OS — simple enough for everyone.** · [furtka.org](https://furtka.org)

View file

@ -13,6 +13,17 @@ hostname=$(cat /etc/hostname 2>/dev/null || hostname)
mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1) mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
new=$( new=$(
# Furtka mark (docs/brand/console-banner.txt, from brand-assets.py)
echo ' ▄▄▄▄▄▄▄▄'
echo ' ▄██████████▄'
echo ' █████▀▀▀▀▀▀███'
echo ' ████▀ ████'
echo ' ████ █████████'
echo ' ████ ████'
echo ' ████ ▄▄▄▄▄████'
echo ' ████ █████████'
echo ' ████ █████████'
echo ' ▀▀▀▀ ▀▀▀▀▀▀▀▀▀'
echo echo
echo " Furtka is ready." echo " Furtka is ready."
echo echo

1
assets/www/favicon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>svg{color:#c03a28}@media (prefers-color-scheme:dark){svg{color:#ff6b56}}</style><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View file

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Install local HTTPS · Furtka</title> <title>Install local HTTPS · Furtka</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

View file

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Furtka</title> <title>Furtka</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

View file

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Settings · Furtka</title> <title>Settings · Furtka</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

View file

@ -73,13 +73,17 @@ body {
align-items: center; align-items: center;
gap: 0.55rem; gap: 0.55rem;
} }
/* Furtka mark (M), drawn as a mask so it takes the accent colour in both
themes. Generated geometry: website/tools/brand-assets.py. */
.brand::before { .brand::before {
content: ""; content: "";
width: 0.7rem; width: 1.35rem;
height: 0.7rem; height: 1.35rem;
background: var(--accent); /* Brand red, not the UI's green accent — the logo keeps its colour.
border-radius: 2px; This stylesheet is dark-first; the light theme gets the deeper red. */
transform: rotate(45deg); background: #ff6b56;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill-rule='evenodd' d='M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill-rule='evenodd' d='M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z'/%3E%3C/svg%3E") center / contain no-repeat;
} }
.nav-links { display: flex; gap: 0.25rem; } .nav-links { display: flex; gap: 0.25rem; }
.nav-links a { .nav-links a {
@ -490,3 +494,8 @@ details.log-details[open] > summary { color: var(--fg); }
color: var(--accent); color: var(--accent);
} }
.app-icon svg { width: 36px; height: 36px; } .app-icon svg { width: 36px; height: 36px; }
.login-brand { margin: 0 0 1.5rem; font-size: 1.2rem; }
@media (prefers-color-scheme: light) {
.brand::before { background: #c03a28; }
}

19
docs/brand/README.md Normal file
View file

@ -0,0 +1,19 @@
# Furtka brand files
The Furtka logo is **Mark M** — a lowercase *f* cut out of an arched gate. It won the
public logo vote on furtka.org (closed 2026-09-28: M 29, R 13 of 42 votes).
| File | Use |
|---|---|
| `furtka-mark.svg` | The mark, 32 px and up |
| `furtka-mark-small.svg` | Simplified drawing for 16–24 px (favicons, tab icons) |
| `furtka-lockup.svg` | Mark + wordmark, accent colour on light grounds |
| `furtka-lockup-white.svg` | Mark + wordmark in white, for dark or accent grounds |
| `console-banner.txt` | Text-mode art for the console welcome screens |
Colours: accent `#c03a28` (dark UI: `#ff6b56`), ink `#0d0d0f`.
Everything here and the copies in `website/static/`, `webinstaller/static/`,
`assets/www/` and `iso/overlay/splash.png` are generated by
`website/tools/brand-assets.py` from the geometry in `website/tools/logo-assets.py`.
Change the geometry there and re-run the script — don't edit the outputs by hand.

View file

@ -0,0 +1,10 @@
▄▄▄▄▄▄▄▄
▄██████████▄
█████▀▀▀▀▀▀███
████▀ ████
████ █████████
████ ████
████ ▄▄▄▄▄████
████ █████████
████ █████████
▀▀▀▀ ▀▀▀▀▀▀▀▀▀

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 232.44 64.00" role="img" aria-label="Furtka"><g transform="scale(1.0000) translate(-6 -2)"><path fill="#ffffff" fill-rule="evenodd" d="M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z"/></g><g fill="#ffffff"><path transform="translate(68.24 58.00) scale(0.05354 -0.05354)" d="M115 0V494H30V600H115V637Q115 687 132.5 724.0Q150 761 192.0 781.5Q234 802 305 802Q333 802 358 799V695Q344 698 324 698Q287 698 273.5 682.0Q260 666 260 632V600H358V494H260V0Z"/><path transform="translate(87.21 58.00) scale(0.05354 -0.05354)" d="M271 -15Q169 -15 119.0 48.5Q69 112 69 218V600H218V244Q218 179 241.5 143.0Q265 107 316 107Q379 107 413.0 156.0Q447 205 447 276V600H596V0H462V90H438Q423 44 382.5 14.5Q342 -15 271 -15Z"/><path transform="translate(121.45 58.00) scale(0.05354 -0.05354)" d="M76 0V600H211V492H235Q241 521 257.0 547.0Q273 573 307.0 589.5Q341 606 399 606H428V471H387Q301 471 263.0 427.0Q225 383 225 294V0Z"/><path transform="translate(144.17 58.00) scale(0.05354 -0.05354)" d="M162 0Q139 27 125.5 65.0Q112 103 112 179V494H30V600H114V750H259V600H358V494H260V196Q260 116 278.0 74.5Q296 33 321 6V0Z"/><path transform="translate(163.14 58.00) scale(0.05354 -0.05354)" d="M78 0V800H226V366H274L449 600H608V594L397 320L632 6V0H457L274 254H226V0Z"/><path transform="translate(195.88 58.00) scale(0.05354 -0.05354)" d="M280 -13Q198 -13 144.0 27.5Q90 68 63.5 138.5Q37 209 37 301Q37 395 64.0 465.0Q91 535 145.0 574.0Q199 613 280 613Q344 613 384.5 587.5Q425 562 445 522H469V600H606V0H469V78H445Q425 41 385.0 14.0Q345 -13 280 -13ZM317 95Q381 95 419.5 151.0Q458 207 458 300Q458 394 419.5 449.5Q381 505 317 505Q257 505 223.5 453.0Q190 401 190 300Q190 199 223.5 147.0Q257 95 317 95Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 232.44 64.00" role="img" aria-label="Furtka"><g transform="scale(1.0000) translate(-6 -2)"><path fill="#c03a28" fill-rule="evenodd" d="M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z"/></g><g fill="#c03a28"><path transform="translate(68.24 58.00) scale(0.05354 -0.05354)" d="M115 0V494H30V600H115V637Q115 687 132.5 724.0Q150 761 192.0 781.5Q234 802 305 802Q333 802 358 799V695Q344 698 324 698Q287 698 273.5 682.0Q260 666 260 632V600H358V494H260V0Z"/><path transform="translate(87.21 58.00) scale(0.05354 -0.05354)" d="M271 -15Q169 -15 119.0 48.5Q69 112 69 218V600H218V244Q218 179 241.5 143.0Q265 107 316 107Q379 107 413.0 156.0Q447 205 447 276V600H596V0H462V90H438Q423 44 382.5 14.5Q342 -15 271 -15Z"/><path transform="translate(121.45 58.00) scale(0.05354 -0.05354)" d="M76 0V600H211V492H235Q241 521 257.0 547.0Q273 573 307.0 589.5Q341 606 399 606H428V471H387Q301 471 263.0 427.0Q225 383 225 294V0Z"/><path transform="translate(144.17 58.00) scale(0.05354 -0.05354)" d="M162 0Q139 27 125.5 65.0Q112 103 112 179V494H30V600H114V750H259V600H358V494H260V196Q260 116 278.0 74.5Q296 33 321 6V0Z"/><path transform="translate(163.14 58.00) scale(0.05354 -0.05354)" d="M78 0V800H226V366H274L449 600H608V594L397 320L632 6V0H457L274 254H226V0Z"/><path transform="translate(195.88 58.00) scale(0.05354 -0.05354)" d="M280 -13Q198 -13 144.0 27.5Q90 68 63.5 138.5Q37 209 37 301Q37 395 64.0 465.0Q91 535 145.0 574.0Q199 613 280 613Q344 613 384.5 587.5Q425 562 445 522H469V600H606V0H469V78H445Q425 41 385.0 14.0Q345 -13 280 -13ZM317 95Q381 95 419.5 151.0Q458 207 458 300Q458 394 419.5 449.5Q381 505 317 505Q257 505 223.5 453.0Q190 401 190 300Q190 199 223.5 147.0Q257 95 317 95Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#c03a28" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#c03a28" fill-rule="evenodd" d="M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z"/></svg>

After

Width:  |  Height:  |  Size: 236 B

View file

@ -73,6 +73,7 @@ _HTML = """<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Furtka Apps</title> <title>Furtka Apps</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
@ -557,12 +558,14 @@ _HTML_LOGIN = """<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Furtka · {{ TITLE }}</title> <title>Furtka · {{ TITLE }}</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
</head> </head>
<body> <body>
<main class="wrap login-wrap"> <main class="wrap login-wrap">
<p class="brand login-brand">Furtka</p>
<h1>{{ HEADING }}</h1> <h1>{{ HEADING }}</h1>
<p class="lede">{{ LEDE }}</p> <p class="lede">{{ LEDE }}</p>
<form id="login-form" onsubmit="return doLogin(event)"> <form id="login-form" onsubmit="return doLogin(event)">

View file

@ -55,6 +55,9 @@ cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/"
rm -rf "$PROFILE_WORK/airootfs/etc/systemd/system/getty@tty1.service.d" rm -rf "$PROFILE_WORK/airootfs/etc/systemd/system/getty@tty1.service.d"
echo "==> Rebranding boot menu (GRUB + syslinux + systemd-boot)" echo "==> Rebranding boot menu (GRUB + syslinux + systemd-boot)"
# BIOS boot menu background: Furtka lockup instead of the Arch logo
# (generated by website/tools/brand-assets.py, 640x480 as syslinux wants).
cp "$SCRIPT_DIR/overlay/splash.png" "$PROFILE_WORK/syslinux/splash.png"
# releng ships menu entries labelled "Arch Linux install medium" across three # releng ships menu entries labelled "Arch Linux install medium" across three
# bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to # bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to
# our brand. Done with sed (not a static overlay) so upstream archiso file # our brand. Done with sed (not a static overlay) so upstream archiso file

View file

@ -16,6 +16,17 @@ issue=/etc/issue
mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1) mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
new=$( new=$(
# Furtka mark (docs/brand/console-banner.txt, from brand-assets.py)
echo ' ▄▄▄▄▄▄▄▄'
echo ' ▄██████████▄'
echo ' █████▀▀▀▀▀▀███'
echo ' ████▀ ████'
echo ' ████ █████████'
echo ' ████ ████'
echo ' ████ ▄▄▄▄▄████'
echo ' ████ █████████'
echo ' ████ █████████'
echo ' ▀▀▀▀ ▀▀▀▀▀▀▀▀▀'
echo echo
echo " Open Furtka in a browser on another device on your network:" echo " Open Furtka in a browser on another device on your network:"
echo echo

BIN
iso/overlay/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>svg{color:#c03a28}@media (prefers-color-scheme:dark){svg{color:#ff6b56}}</style><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View file

@ -4,6 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Furtka Installer{% endblock %}</title> <title>{% block title %}Furtka Installer{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block head_extra %}{% endblock %} {% block head_extra %}{% endblock %}
</head> </head>
@ -11,12 +12,8 @@
<header class="site-header"> <header class="site-header">
<div class="container"> <div class="container">
<a href="{{ url_for('install_step_1') }}" class="site-title" aria-label="Furtka Installer — restart"> <a href="{{ url_for('install_step_1') }}" class="site-title" aria-label="Furtka Installer — restart">
<svg class="gate-mark" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg class="gate-mark" width="24" height="24" viewBox="0 0 64 64" aria-hidden="true">
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/>
<path d="M4 20 V12 A9 9 0 0 1 20 12 V20"/>
<line x1="12" y1="5" x2="12" y2="20"/>
<line x1="3" y1="20" x2="21" y2="20"/>
<line x1="15" y1="12" x2="15" y2="14.5"/>
</svg> </svg>
<span class="wordmark">Furtka<span class="sep">·</span>Installer</span> <span class="wordmark">Furtka<span class="sep">·</span>Installer</span>
</a> </a>

View file

@ -693,6 +693,16 @@ kbd {
.poll-bar { height: 0.45rem; border-radius: 999px; background: var(--bg-subtle); overflow: hidden; display: none; } .poll-bar { height: 0.45rem; border-radius: 999px; background: var(--bg-subtle); overflow: hidden; display: none; }
.poll-bar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width 500ms ease; } .poll-bar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width 500ms ease; }
.poll--voted .poll-bar { display: block; } .poll--voted .poll-bar { display: block; }
.poll-bar--final { display: block; }
.poll-result {
margin: 1.5rem 0 0;
padding: 1rem 1.2rem;
border-left: 3px solid var(--accent);
background: var(--bg-subtle);
border-radius: 0 0.6rem 0.6rem 0;
max-width: 40rem;
}
.poll-result strong { color: var(--accent); }
.poll-count { margin: 0; min-height: 1.4em; font-size: 0.85rem; color: var(--fg-muted); font-variant-numeric: tabular-nums; } .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.25rem 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 { margin-top: 1rem; max-width: var(--measure); font-size: 0.85rem; color: var(--fg-muted); }

View file

@ -5,7 +5,7 @@ status_text: "in Arbeit"
cta_download: "ISO herunterladen" cta_download: "ISO herunterladen"
cta_source: "Quellcode" cta_source: "Quellcode"
poll_link: "/de/logo/" poll_link: "/de/logo/"
poll_text: "Wir suchen ein Logo — hilf uns beim Aussuchen" poll_text: "Abgestimmt — das ist unser neues Logo"
# features_today / features_next müssen index-parallel zu content/_index.md bleiben. # features_today / features_next müssen index-parallel zu content/_index.md bleiben.
intro: | intro: |
**Furtka** ist ein offenes Heimserver-Betriebssystem. **Furtka** ist ein offenes Heimserver-Betriebssystem.

View file

@ -5,7 +5,7 @@ status_text: "work in progress"
cta_download: "Download ISO" cta_download: "Download ISO"
cta_source: "Source code" cta_source: "Source code"
poll_link: "/logo/" poll_link: "/logo/"
poll_text: "We're picking a logo — help us choose" poll_text: "The vote is in — meet our new logo"
# Keep features_today / features_next index-aligned with content/_index.de.md. # Keep features_today / features_next index-aligned with content/_index.de.md.
intro: | intro: |
**Furtka** is an open-source home server OS. **Furtka** is an open-source home server OS.

View file

@ -1,6 +1,11 @@
--- ---
title: "Welches Logo?" title: "Unser neues Logo"
description: "Zwei Vorschläge für das Furtka-Zeichen. Wähl das, das dir besser gefällt — ein Klick genügt." description: "Zwei Vorschläge für das Furtka-Zeichen, und ihr habt gewählt. So ist die Abstimmung ausgegangen — und so sähe jedes Zeichen überall aus, wo es funktionieren müsste."
closed: true
result: "Die Abstimmung ist beendet — **M hat mit 29 von 42 Stimmen gewonnen** (R: 13). Danke an alle, die abgestimmt haben! M ist jetzt das Furtka-Logo."
final:
m: 29
r: 13
translationKey: "logo" translationKey: "logo"
layout: single layout: single
type: poll type: poll
@ -17,7 +22,7 @@ r:
title: "R — Fenster, Rad und F" title: "R — Fenster, Rad und F"
text: "Ein Bogenfenster mit einem Speichenrad dahinter; in der oberen rechten Scheibe wird die Speiche zum F." text: "Ein Bogenfenster mit einem Speichenrad dahinter; in der oberen rechten Scheibe wird die Speiche zum F."
show: show:
intro: "Noch unentschieden? Hier ist jedes Zeichen überall dort, wo es funktionieren muss." intro: "Beide Zeichen überall dort, wo sie hätten funktionieren müssen."
sizes: "Groß und klein" 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." 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: "Mit dem Namen"
@ -48,6 +53,6 @@ labels:
*Furtka* ist polnisch für die kleine Gartenpforte — die Tür im Zaun, durch *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 die man hineinkommt, ohne das große Tor aufzumachen. Beide Zeichen spielen
damit: das eine zeichnet den Buchstaben ins Tor, das andere schaut durchs damit: das eine zeichnet den Buchstaben ins Tor, das andere schaut durchs
Tor auf ein Rad. Stimm direkt hier ab — oder scroll erst weiter und sieh Tor auf ein Rad. Unten stehen beide mit dem Endergebnis — und jedes Zeichen
beide Zeichen überall, wo sie funktionieren müssen, vom Poster bis zum überall dort, wo es hätte funktionieren müssen, vom Poster bis zum winzigen
winzigen Symbol im Browser-Tab. Symbol im Browser-Tab.

View file

@ -1,6 +1,11 @@
--- ---
title: "Which logo?" title: "Our new logo"
description: "Two proposals for the Furtka mark. Pick the one you like better — it takes one click." description: "Two proposals for the Furtka mark, and you picked one. Here is how the vote ended — and each mark everywhere it would have had to work."
closed: true
result: "The vote is closed — **M won with 29 of 42 votes** (R: 13). Thank you to everyone who voted! M is now the Furtka logo."
final:
m: 29
r: 13
translationKey: "logo" translationKey: "logo"
url: /logo/ url: /logo/
layout: single layout: single
@ -18,7 +23,7 @@ r:
title: "R — window, wheel and F" 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." text: "An arched window with a wheel of spokes behind it; in the top-right pane the spoke becomes an F."
show: show:
intro: "Not sure yet? Here is each mark everywhere it would have to work." intro: "Both marks everywhere they would have had to work."
sizes: "Big and small" 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." 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: "With the name"
@ -49,5 +54,5 @@ labels:
*Furtka* is Polish for a small garden gate — the little door in the fence *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: 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 one draws the letter into the gate, the other looks through the gate at a
wheel. Vote right here — or scroll on first and see both marks everywhere wheel. Below are both, with the final count — and each mark everywhere it
they would have to work, from a poster to the tiny icon in a browser tab. would have had to work, from a poster to the tiny icon in a browser tab.

View file

@ -14,7 +14,7 @@
<a class="cta cta--ghost" href="{{ site.Params.forgejoURL }}/{{ site.Params.repoPath }}">{{ .Params.cta_source }}</a> <a class="cta cta--ghost" href="{{ site.Params.forgejoURL }}/{{ site.Params.repoPath }}">{{ .Params.cta_source }}</a>
</p> </p>
{{ with .Params.poll_link }} {{ with .Params.poll_link }}
<p class="reveal"><a class="home-poll" href="{{ . }}">{{ partial "mark-m.html" (dict "class" "" "id" "hm") }}{{ partial "mark-r.html" (dict "class" "" "id" "hr") }}<span>{{ $.Params.poll_text }} →</span></a></p> <p class="reveal"><a class="home-poll" href="{{ . }}">{{ partial "mark-m.html" (dict "class" "" "id" "hm") }}<span>{{ $.Params.poll_text }} →</span></a></p>
{{ end }} {{ end }}
</header> </header>

View file

@ -1,6 +1,2 @@
<svg class="gate-mark" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> {{- /* Furtka mark (M, small-size tier) — generated by tools/brand-assets.py from logo-assets.py geometry. */ -}}
<path d="M4 20 V12 A9 9 0 0 1 20 12 V20"/> <svg class="gate-mark" width="1em" height="1em" viewBox="0 0 64 64" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>
<line x1="12" y1="5" x2="12" y2="20"/>
<line x1="3" y1="20" x2="21" y2="20"/>
<line x1="15" y1="12" x2="15" y2="14.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 390 B

View file

@ -10,12 +10,13 @@
<meta name="theme-color" content="#f7f6f3" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#f7f6f3" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0d0d0f" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#0d0d0f" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta property="og:site_name" content="{{ site.Title }}"> <meta property="og:site_name" content="{{ site.Title }}">
<meta property="og:title" content="{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}"> <meta property="og:title" content="{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}">
<meta property="og:description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ site.Params.description }}{{ end }}"> <meta property="og:description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ site.Params.description }}{{ end }}">
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}"> <meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:url" content="{{ .Permalink }}"> <meta property="og:url" content="{{ .Permalink }}">
{{ with .Params.image }}<meta property="og:image" content="{{ . | absURL }}"> {{ with .Params.image | default "/img/brand/social.png" }}<meta property="og:image" content="{{ . | absURL }}">
<meta property="og:image:width" content="1200"> <meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630"> <meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">{{ end }} <meta name="twitter:card" content="summary_large_image">{{ end }}

View file

@ -4,32 +4,43 @@
side gets the "first thing on the page" advantage for the whole poll. */}} side gets the "first thing on the page" advantage for the whole poll. */}}
{{- $order := $p.order | default (slice "m" "r") -}} {{- $order := $p.order | default (slice "m" "r") -}}
{{- $s := $p.show -}} {{- $s := $p.show -}}
<article class="poll" data-poll="{{ $p.poll }}" data-api="/api/poll/{{ $p.poll }}"> <article class="{{ if $p.closed }}poll-closed{{ else }}poll{{ end }}" data-poll="{{ $p.poll }}" data-api="/api/poll/{{ $p.poll }}">
<header class="page-header"> <header class="page-header">
<h1>{{ .Title }}</h1> <h1>{{ .Title }}</h1>
{{ with $p.description }}<p class="lede">{{ . }}</p>{{ end }} {{ with $p.description }}<p class="lede">{{ . }}</p>{{ end }}
</header> </header>
{{ with $p.result }}<p class="poll-result" role="status">{{ . | markdownify }}</p>{{ end }}
<div class="prose">{{ .Content }}</div> <div class="prose">{{ .Content }}</div>
{{/* ── Quick vote ─────────────────────────────────────────── */}} {{/* ── Quick vote ─────────────────────────────────────────── */}}
<div class="poll-grid" id="vote"> <div class="poll-grid" id="vote">
{{ range $k := $order }}{{ $d := index site.Data.logo $k }} {{ range $k := $order }}{{ $d := index site.Data.logo $k }}
{{ $c := index $p $k }} {{ $c := index $p $k }}
<section class="poll-card" data-choice="{{ $k }}" aria-labelledby="poll-{{ $k }}-title"> {{- $total := 0 }}{{ range $v := $p.final }}{{ $total = add $total $v }}{{ end }}
{{- $won := and $p.closed (eq $k (cond (gt (index $p.final "m") (index $p.final "r")) "m" "r")) }}
<section class="poll-card{{ if $won }} is-yours{{ end }}" data-choice="{{ $k }}" aria-labelledby="poll-{{ $k }}-title">
<div class="poll-art poll-art--{{ $k }}">{{ $d.display | safeHTML }}</div> <div class="poll-art poll-art--{{ $k }}">{{ $d.display | safeHTML }}</div>
<h2 id="poll-{{ $k }}-title">{{ $c.title }}</h2> <h2 id="poll-{{ $k }}-title">{{ $c.title }}</h2>
<p class="poll-desc">{{ $c.text }}</p> <p class="poll-desc">{{ $c.text }}</p>
{{ if $p.closed }}
{{ $n := index $p.final $k }}
<div class="poll-bar poll-bar--final" aria-hidden="true"><i style="width: {{ div (mul $n 100) $total }}%"></i></div>
<p class="poll-count">{{ $n }} / {{ $total }}</p>
{{ else }}
<button type="button" class="poll-btn" data-vote="{{ $k }}">{{ $p.labels.vote }}</button> <button type="button" class="poll-btn" data-vote="{{ $k }}">{{ $p.labels.vote }}</button>
<div class="poll-bar" aria-hidden="true"><i></i></div> <div class="poll-bar" aria-hidden="true"><i></i></div>
<p class="poll-count" data-count="{{ $k }}"></p> <p class="poll-count" data-count="{{ $k }}"></p>
{{ end }}
</section> </section>
{{ end }} {{ end }}
</div> </div>
{{ if not $p.closed }}
<p class="poll-status" role="status" aria-live="polite" <p class="poll-status" role="status" aria-live="polite"
data-t-voted="{{ $p.labels.voted }}" data-t-voted="{{ $p.labels.voted }}"
data-t-total="{{ $p.labels.total }}" data-t-total="{{ $p.labels.total }}"
data-t-error="{{ $p.labels.error }}" data-t-error="{{ $p.labels.error }}"
data-t-change="{{ $p.labels.change }}">{{ $p.labels.nojs }}</p> data-t-change="{{ $p.labels.change }}">{{ $p.labels.nojs }}</p>
{{ end }}
{{/* ── Showcase ───────────────────────────────────────────── */}} {{/* ── Showcase ───────────────────────────────────────────── */}}
<div class="show"> <div class="show">
@ -148,6 +159,7 @@
</div> </div>
{{/* ── Vote again at the end ─────────────────────────────── */}} {{/* ── Vote again at the end ─────────────────────────────── */}}
{{ if not $p.closed }}
<section class="poll-final"> <section class="poll-final">
<h2 class="show-h">{{ $s.final }}</h2> <h2 class="show-h">{{ $s.final }}</h2>
<div class="poll-final-row"> <div class="poll-final-row">
@ -162,7 +174,10 @@
</div> </div>
</section> </section>
<p class="poll-note">{{ $p.labels.note | safeHTML }}</p> <p class="poll-note">{{ $p.labels.note | safeHTML }}</p>
{{ end }}
</article> </article>
{{ if not $p.closed }}
{{ $js := resources.Get "js/poll.js" | minify | fingerprint }} {{ $js := resources.Get "js/poll.js" | minify | fingerprint }}
<script defer src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script> <script defer src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script>
{{ end }} {{ end }}
{{ end }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,6 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#a33e2e" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>svg{color:#c03a28}@media (prefers-color-scheme:dark){svg{color:#ff6b56}}</style><path fill="currentColor" fill-rule="evenodd" d="M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z"/></svg>
<path d="M4 20 V12 A9 9 0 0 1 20 12 V20"/>
<line x1="12" y1="5" x2="12" y2="20"/>
<line x1="3" y1="20" x2="21" y2="20"/>
<line x1="15" y1="12" x2="15" y2="14.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Generate the Furtka brand files from the winning logo (Mark M) everywhere
it ships: website, live installer, on-box UI, ISO boot splash, repo docs.
python3 website/tools/brand-assets.py
Reuses the geometry and rasteriser of logo-assets.py (the poll showcase), so
the mark is drawn from one source. Needs the same build-only deps: cairosvg,
Pillow, fontTools. Output files are committed; re-run after a geometry change.
"""
import importlib.util
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, "..", ".."))
_spec = importlib.util.spec_from_file_location("logo_assets", os.path.join(HERE, "logo-assets.py"))
la = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(la)
K = "m"
COLOR = la.MARKS[K]["color"] # light-theme accent
COLOR_DARK = "#ff6b56" # dark-theme accent used by the site and the box UI
INK = la.MARKS[K]["ink"]
def out(*parts):
path = os.path.join(REPO, *parts)
os.makedirs(os.path.dirname(path), exist_ok=True)
return path
def favicon_svg():
"""Small-size tier, accent colour, lighter accent on dark browser chrome."""
inner = la.body(K, "min", "currentColor")
x, y, w, h = la.MARKS[K]["vb"]
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {w} {h}">'
f"<style>svg{{color:{COLOR}}}"
f"@media (prefers-color-scheme:dark){{svg{{color:{COLOR_DARK}}}}}</style>"
f"{inner}</svg>\n"
)
def mark_svg(tier, color):
return (
la.svg(K, tier, color, standalone=True)
.replace(' class="pm"', "")
.replace(' aria-hidden="true" focusable="false"', "")
+ "\n"
)
def lockup_svg(color):
s = la.lockup(K).replace("currentColor", color)
return (
s.replace('<svg class="pm"', f"<svg {la.XMLNS}").replace(
' aria-hidden="true" focusable="false"', ' role="img" aria-label="Furtka"'
)
+ "\n"
)
def write(path, text):
with open(path, "w") as f:
f.write(text)
def main():
fav = favicon_svg()
for p in (
("website", "static", "favicon.svg"),
("webinstaller", "static", "favicon.svg"),
("assets", "www", "favicon.svg"),
):
write(out(*p), fav)
la.tile(K, 180, pad=0.2).convert("RGB").save(
out("website", "static", "apple-touch-icon.png"), optimize=True
)
# Default share card for pages without their own og:image.
la.lockup_png(K, 1200, 630, False, 230).convert("RGB").save(
out("website", "static", "img", "brand", "social.png"), optimize=True
)
# syslinux (BIOS boot menu) wants a 640x480 PNG and draws its menu over
# the middle of it, so the lockup sits small in the top band — same
# layout as releng's Arch splash, which build.sh replaces with this.
splash_band = la.lockup_png(K, 640, 120, False, 44)
splash = la.Image.new("RGBA", (640, 480), INK)
splash.alpha_composite(splash_band, (0, 8))
splash.convert("RGB").save(out("iso", "overlay", "splash.png"), optimize=True)
write(out("docs", "brand", "furtka-mark.svg"), mark_svg("flat", COLOR))
write(out("docs", "brand", "furtka-mark-small.svg"), mark_svg("min", COLOR))
write(out("docs", "brand", "furtka-lockup.svg"), lockup_svg(COLOR))
write(out("docs", "brand", "furtka-lockup-white.svg"), lockup_svg("#ffffff"))
write(out("docs", "brand", "console-banner.txt"), la.banner(K, cols=20) + "\n")
print("brand assets ok")
if __name__ == "__main__":
sys.exit(main())