The site wears the product's mark

The nav and footer carried a purple gradient square — a placeholder that
outlived its excuse — and the site had no favicon and no og:image at all, so a
shared link rendered as a bare URL with a blank tab icon.

All of it now comes from one artwork, through `Scripts/make-brand-assets.py`.
🔴 The outputs are generated: an icon edited in place is an icon that silently
stops matching the next time the script runs, which is the same shape as the
generated doc-page mistake this repo already made once.

Two things about the source worth recording. The export carried a leftover dark
matte — RGB (33,33,33) at alpha 38 across the background — invisible on a dark
page and a grey haze on a light one; the script strips anything below alpha 48.
And the wordmark is interlocked with the emblem: "FlameNet" is drawn ON the
globe, and the speech bubbles hang down into the letter tops, so no rectangular
crop gets the whole mark without text. The crop used, (280,0,1040,548), is the
one that takes both bubbles and the flame arc and leaves the wordmark behind.

⚠️ `assetVersion` now hashes the brand images too. Without that a logo change
alone leaves the hash where it was, so every browser that has ever loaded the
site keeps serving the old mark out of cache — which looks exactly like the
deploy not having happened.

⚠️ Not changed, and worth a decision separately: the site's accent is purple
and the logo is blue with an orange flame. They do not clash badly at 30px, but
they are not one palette either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-07 09:28:01 -04:00
co-authored by Claude Opus 5
parent 7e1792f638
commit f9d85e32f7
16 changed files with 124 additions and 5 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

+6 -2
View File
@@ -93,10 +93,14 @@ a { color: inherit; }
font-weight: 700; letter-spacing: -.02em; font-size: 17px;
text-decoration: none; flex: none;
}
/* The product's mark, not a coloured square. The gradient block that used to
sit here was a placeholder that outlived its excuse. */
.mark {
width: 30px; height: 30px; border-radius: 9px; flex: none;
background: var(--grad);
box-shadow: 0 4px 12px rgba(88,66,188,.34);
/* The artwork is painted on the panel navy, so the radius has to clip it —
without this the square corners show against the page. */
object-fit: cover; display: block;
box-shadow: 0 4px 12px rgba(18,26,46,.28);
}
.nav-links { display: flex; align-items: center; justify-content: center; gap: 34px; flex: 1; }
+10 -2
View File
@@ -8,6 +8,12 @@
<meta property="og:title" content="#(title)">
<meta property="og:description" content="#(description)">
<meta property="og:type" content="website">
<meta property="og:image" content="https://flamenetmessenger.com/brand/og.png?v=#(assetVersion)">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="/brand/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/brand/favicon-32.png?v=#(assetVersion)">
<link rel="icon" type="image/png" sizes="16x16" href="/brand/favicon-16.png?v=#(assetVersion)">
<link rel="apple-touch-icon" href="/brand/apple-touch-icon.png?v=#(assetVersion)">
<link rel="stylesheet" href="/landing.css?v=#(assetVersion)">
</head>
<body>
@@ -25,7 +31,8 @@
<nav class="nav">
<div class="wrap nav-inner">
<a class="brand" href="/">
<span class="mark" aria-hidden="true"></span>
<img class="mark" src="/brand/mark-128.png?v=#(assetVersion)" alt="" aria-hidden="true"
width="30" height="30">
<span>Flamenet&nbsp;Messenger</span>
</a>
<button class="nav-toggle" aria-expanded="false" aria-controls="nav-links" aria-label="Menu">
@@ -64,7 +71,8 @@
<div class="footer-cols">
<div class="footer-brand">
<span class="mark" aria-hidden="true"></span>
<img class="mark" src="/brand/mark-128.png?v=#(assetVersion)" alt="" aria-hidden="true"
width="30" height="30">
<span>Flamenet&nbsp;Messenger</span>
</div>
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Every icon and favicon, derived from one source artwork.
Run it, do not hand-edit the outputs — an icon edited in place is an icon that
silently stops matching the next time this runs, which is the same shape as the
generated doc pages this repo already got wrong once.
python3 Scripts/make-brand-assets.py
It writes into three repositories, because one logo has to be the same logo in
all three: this repo's Public/brand, the iPhone app's asset catalogue, and the
Android app's mipmaps. Paths below assume they are siblings under ~/Code.
Two rules that are not obvious and both cost a rejected build if broken:
- 🔴 The iOS icon must have NO alpha channel. App Store Connect rejects an icon
that has one, and the rejection arrives after the upload rather than during
the build.
- 🔴 The Android foreground must sit inside the adaptive-icon safe zone (72 of
108), or a launcher with a circular mask cuts the flame arc off the mark.
"""
from PIL import Image
import os
HOME = os.path.expanduser("~/Code")
SRC = f"{HOME}/flamenet-relay/brand/flamenet-messenger-logo.png"
NAVY = (18, 26, 46) # #121A2E — the app palette's panel navy
CROP = (280, 0, 1040, 548) # the emblem: globe, both bubbles, the flame arc
PAD = 0.06 # breathing room so nothing sits flush to the edge
def master():
"""The source with its leftover matte removed.
The export carried a dark grey (33,33,33) at alpha 38 across the
background — invisible on a dark page, a grey haze on a light one.
"""
src = Image.open(SRC).convert("RGBA")
r, g, b, a = src.split()
a = a.point(lambda v: 0 if v < 48 else v)
return Image.merge("RGBA", (r, g, b, a))
M = master()
def emblem(size, background=None, content_fraction=1.0):
"""The square mark. `background=None` leaves it transparent."""
c = M.crop(CROP)
side = int(max(c.size) * (1 + PAD))
fill = (background[0], background[1], background[2], 255) if background else (0, 0, 0, 0)
canvas = Image.new("RGBA", (side, side), fill)
canvas.alpha_composite(c, ((side - c.size[0]) // 2, (side - c.size[1]) // 2))
out = canvas.resize((size, size), Image.LANCZOS)
if content_fraction >= 1.0:
return out
# Shrink inside the canvas — for Android's adaptive-icon safe zone.
inner = int(size * content_fraction)
small = out.resize((inner, inner), Image.LANCZOS)
framed = Image.new("RGBA", (size, size), (0, 0, 0, 0))
framed.alpha_composite(small, ((size - inner) // 2, (size - inner) // 2))
return framed
def wordmark(width):
c = M.crop(M.split()[3].getbbox())
h = round(c.size[1] * width / c.size[0])
return c.resize((width, h), Image.LANCZOS)
def save(img, path):
os.makedirs(os.path.dirname(path), exist_ok=True)
img.save(path)
print(f"{path} {img.size[0]}x{img.size[1]} alpha={'yes' if img.mode=='RGBA' else 'no'}")
# ---------------------------------------------------------------- public pages
P = f"{HOME}/flamenet-relay/Public/brand"
save(wordmark(1100), f"{P}/logo.png")
for s in (64, 128, 256, 512):
save(emblem(s, NAVY), f"{P}/mark-{s}.png")
save(emblem(180, NAVY), f"{P}/apple-touch-icon.png")
for s in (16, 32, 48):
save(emblem(s, NAVY), f"{P}/favicon-{s}.png")
ico = emblem(256, NAVY).convert("RGB")
ico.save(f"{P}/favicon.ico", sizes=[(16,16),(32,32),(48,48)])
print(f"{P}/favicon.ico multi-size")
# The social card: the full logo on the panel navy, at the size every
# platform crops from.
card = Image.new("RGBA", (1200, 630), NAVY + (255,))
w = wordmark(760)
card.alpha_composite(w, ((1200 - 760) // 2, (630 - w.size[1]) // 2))
save(card.convert("RGB"), f"{P}/og.png")
# ------------------------------------------------------------------------- iOS
# 🔴 No alpha. App Store Connect rejects an icon with an alpha channel.
ios = emblem(1024, NAVY).convert("RGB")
save(ios, f"{HOME}/flamenetmessenger/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png")
# --------------------------------------------------------------------- Android
# Adaptive foreground: transparent, content inside the 66/108 safe zone so a
# circular or squircle mask cannot cut the mark.
A = f"{HOME}/flamenet-messenger-android/app/src/main/res"
for bucket, px in [("mdpi",108), ("hdpi",162), ("xhdpi",216), ("xxhdpi",324), ("xxxhdpi",432)]:
save(emblem(px, None, content_fraction=0.72), f"{A}/mipmap-{bucket}/ic_launcher_foreground.png")
+7 -1
View File
@@ -1312,7 +1312,13 @@ final class BootTask: LifecycleHandler, @unchecked Sendable {
/// boot from the file contents, so it changes exactly when they do.
enum AssetVersion {
static let value: String = {
let paths = ["Public/landing.css", "Public/landing.js", "Public/donate.js"]
// ⚠️ The brand images are in here too, and belong here. Without them a
// logo change alone leaves the hash where it was, so every browser
// that has ever loaded the site keeps serving the old mark from cache
// — which looks exactly like the deploy not having happened.
let paths = ["Public/landing.css", "Public/landing.js", "Public/donate.js",
"Public/brand/mark-128.png", "Public/brand/og.png",
"Public/brand/apple-touch-icon.png"]
var hasher = SHA256()
for path in paths {
if let data = FileManager.default.contents(atPath: path) { hasher.update(data: data) }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB