marketing/facebook: the mark alone on the profile, the real lettering on the covers
gates / gates (push) Successful in 4m28s

Operator refinements after looking at the rebuilt pictures. No geometry changed:
R-919's measured constants, its band and every check stand, and the red-proof
still convicts the old layout.

Profile picture: the logo MARK only. The mark plus the "felhom.eu" lettering was
too much for the 176 px Facebook shows -- and smaller than the shape the Page
carried before this work, because build.py shrinks the artwork to fit inside the
circle where the original was simply cropped by it. Dropping the lettering grows
the mark from 69% to 76% of the circle; canvas 932 -> 648 and the profile_width
floor 720 -> 640 (twice Meta's recommended 320 source; 720 was above what the
mark alone needs and would have shrunk it for nothing). logo.png is split at a
MEASURED row: ink y 5-289, blank band y 290-295, wordmark y 296-403.

Covers: the headline is now set the way the WEBSITE sets its h1. site.css
.page-index .hero-text h1 is font-weight 700, letter-spacing -0.03em; this file
used ExtraBold 800 with no tracking, so the cover did not actually match the
page. HEADLINE_WEIGHT/HEADLINE_TRACK now carry those, with real per-glyph
spacing. Bold is narrower so headlines grew: A 57->62, B 58->63, C 47->50.

"felhom.eu" is no longer typed anywhere: wordmark() draws the logo's own
lettering, which is set in "M+ 2c"/"Vremena Grotesk" -- fonts this machine does
not have (R-916) -- so any typed version was a look-alike. The website hero
shows the same logo.png on the same dark background, so the covers match the
page. The DOMAIN constant is removed rather than left as dead code.

Caught before it shipped: the preview's new profile paragraph added a fourth %d
and the argument tuple stayed in the old order, so the phone paragraph rendered
"the centre 76 px of the 938-pixel width, with a profile circle 1640
cover-pixels across". Every number was real, just in the wrong slot, which is
why it read as plausible instead of crashing. Found by reading the rendered
paragraph back, not by the exit code.

Built on DooPlex; all checks pass, the phone simulation still shows 0 px cut and
0 px under the profile circle on all three covers. R-919 stays VERIFY.
This commit is contained in:
2026-10-09 07:29:56 +02:00
parent 702e19ee58
commit ba7174012a
11 changed files with 197 additions and 40 deletions
+37
View File
@@ -177,6 +177,43 @@ Two notes on how that was read, because the obvious ways are both wrong here:
icon is attributed to a neighbour. Reading the icon off a neighbouring row is exactly how a red run gets
reported green, so it is written down.
## 11. Operator refinements, same day (2026-10-09)
After looking at the rebuilt pictures the operator asked for two things. **No geometry changed** — R-919's
measured constants, its band and all its checks stand, and the red-proof still convicts the old layout.
**1. The profile picture carries the logo mark only.** The observation was right: the mark plus the
„felhom.eu" lettering was too much for 176 px, and smaller than what the Page carried before this work —
because `build.py` shrinks the artwork until it fits inside the circle, where the original was simply
cropped by it. Dropping the lettering lets the mark grow from **69 % to 76 %** of the circle; the canvas
goes 932 → **648 px** and the floor in `profile_width` 720 → **640** (twice Meta's recommended 320 source;
720 was above what the mark alone needs and would have shrunk it for no gain). `logo.png` is split at a
**measured** row — ink y 5–289, blank band y 290–295, wordmark y 296–403 — not at a guessed fraction.
Both backgrounds now read at 40 px, which was not true before, so the preview's „dark recommended" line
lost its old reason and now states the real trade-off.
**2. The covers use the website's own headline setting, and the real wordmark.** Read out of
`website/assets/site.css`, `.page-index .hero-text h1` is `font-weight: 700; letter-spacing: -0.03em`.
`build.py` was using **ExtraBold (800) with no tracking** — so the cover did not in fact match the page.
It now sets `HEADLINE_WEIGHT = "Bold"` and `HEADLINE_TRACK = -0.03`, with genuine per-glyph spacing.
Bold is narrower, so the headlines grew: A 57 → 62, B 58 → 63, C 47 → 50 (capitals 48, 49, 39 px against
the 25 px floor). „felhom.eu" is no longer typed at all: `wordmark()` draws the logo's own lettering, which
is set in „M+ 2c" / „Vremena Grotesk" — fonts this machine does not have (R-916), so any typed version was
a look-alike. The website hero shows the same `logo.png` on the same dark background, so the covers now
match the page exactly. The `DOMAIN` constant was removed rather than left as dead code implying otherwise.
**One mistake worth recording, caught before it shipped.** The preview page's new profile paragraph added a
fourth `%d` and I left the argument tuple in the old order, so the phone paragraph rendered „the centre
**76** px of the **938**-pixel width, with a profile circle **1640** cover-pixels across." Every number was a
real number from the file, just the wrong one in each slot — which is exactly why it read as plausible
instead of crashing. Found by reading the rendered paragraph back rather than trusting the build's exit
code; fixed, and both paragraphs re-read afterwards.
**Rebuilt and verified:** all checks pass on DooPlex, the phone simulation still shows 0 px cut and 0 px
under the profile circle on all three covers, and the renders were looked at at 1640 px, at phone width and
at 176/96/40 px. New profile hashes: `db05dae5…` dark, `a44849e4…` white — they changed because changing
them was the request. R-919 stays **VERIFY**: the operator still uploads and checks in the Facebook app.
## 10. Teardown
Nothing on any host, the hub or Facebook. The work ran in the two git working trees only; the throwaway
+1 -1
View File
@@ -278,7 +278,7 @@ stopping line that lies.
| **R-915** | Business & legal | P4 | **The Meta app `felhom.eu` is in development mode, so posts it makes are seen only by people with a role on the app.** READ 2026-10-08 (Meta docs, cited in the spike); not measured. MEASURED: development mode does not refuse posting (both test writes HTTP 200). Live needs display name, contact e-mail, a Terms of Service URL, an app icon, a category and the app purpose (privacy-policy and data-deletion URLs listed beside them). The robot's Page assignment, missing at first, was done by the operator the same day. | **WAITING-ON-OPERATOR** | R-813 (the Terms of Service URL) | Before real public posts: switch the app to Live in the Meta developer page. If nothing is done: posts stay invisible to the public. After Live: CC adds the Page link to the website footer (not before — a link to a Page nobody can read is worse than none) | operator |
| **R-916** | Business & legal | P4 | **The logo has no usable vector master: `website/assets/logo.svg` sets „felhom.eu" as live text in the fonts „M+ 2c" and „Vremena Grotesk", which DooPlex does not have, so every renderer here draws other letters.** SEEN 2026-10-08 (Facebook pictures task): librsvg drew the lettering in DejaVu; `fc-match` resolves the family to DejaVu Sans. The PNG (645 x 408) is the only faithful copy, which caps every picture made from it at about that size (`marketing/facebook/README.md`). | **WAITING-ON-OPERATOR** | the machine with the fonts | In Inkscape on that machine: select the lettering → Path → Object to Path → save as `website/assets/logo-master.svg`; then `marketing/facebook/build.py` can use it. If nothing is done: the PNG stays the master; larger prints will be soft | operator |
| **R-917** | Business & legal | P4 | **`COPY.md` §2, the Page's longer description (867 characters), has nowhere to go: Facebook's current Pages experience has no long-description field at all.** FOUND 2026-10-08 (Page setup task, `audits/facebook-page-setup-2026-10-08/`). Looked in four places, all on the live Page as its admin: the Page's „Névjegy" tab (Rövid áttekintés / Személyes adatok / Részletek — only a 255-character „Bemutatkozás" and the pinned category), Business Suite's „Oldal módosítása" dialog (profile picture, cover, Bemutatkozás, category, phone, e-mail, address, website, social links — and nothing else), Facebook settings → „Oldal adatai" (redirects to the same Névjegy tab) and settings → „Oldal beállítása" (name, access, type, history, status, recommendation, messaging, data sharing). MEASURED by Graph with the Page token: `description`, `general_info` and `bio` all read `null`. Writing `description` by API would need `pages_manage_metadata`; the robot key's scopes are `read_insights, pages_show_list, business_management, pages_read_engagement, pages_read_user_content, pages_manage_posts, pages_manage_engagement, public_profile`, and the task's fences forbid adding a permission. So §2 is written, reviewed and unplaceable. **Options for the operator:** (a) leave §2 unused and let the 99-character intro plus the website carry it; (b) shorten §2 to ≤ 255 characters and make it the „Bemutatkozás" instead of §1 — but §1 was written for exactly that slot, so this is really „rewrite one of the two"; (c) publish §2 as the Page's first pinned post once the app is Live (R-915), which is where a long text actually gets read; (d) ask Meta support whether the field still exists for this Page type. Recommended (c) — the text reads like a post already. | **WAITING-ON-OPERATOR** | the choice a/b/c/d; (c) also waits on R-915 | Pick a/b/c/d. If nothing is done: §2 stays in `COPY.md` unused and the Page carries only the 99-character intro | operator |
| **R-919** | Business & legal | P3 | **On a phone the Facebook Page cuts the left edge of the cover: the „s” of „saját szabályaid” and the „f” of „felhom.eu” are gone.** MEASURED 2026-10-08 on the live Page in Chrome DevTools device mode, Pixel 9 (412 × 924, mobile user agent, after a reload so Facebook serves the mobile bundle): `audits/facebook-page-setup-2026-10-08/C2-phone-headline-cut-closeup.png`. The mobile Page header is **412 × 274 = 1,504:1**, so Facebook keeps our cover's full height and shows only the centre **938 px of its 1640 px width (57,2 %)** — **351 px cut from each side**. `marketing/facebook/build.py` builds to a safe area of the centre **1028 × 544** (306 px clear of each edge), which is **45 px wider per side than the phone actually shows**; the light text in `out/cover-c.png` runs from x 333 to x 997, and the left crop edge is x 351, so the first **18 px** of the text are cut. Covers A and B are built from the same safe area and will have the same edge. Two further facts this measurement establishes: **Meta's own help page is wrong about its own rendering** — it states the mobile cover is 2,4:1 where the Page header measures 1,504:1 — and the mobile profile circle is far bigger than assumed (172 px, centred, overlapping the bottom 112 px of the 274 px cover, i.e. source x 637–1030 × y 369–624 is hidden). Not re-cropped on Facebook, per the task's fence. **-- 2026-10-09, FIXED in the build:** `marketing/facebook/build.py` now takes the phone view from the measurement (`PHONE_HDR = (412, 274)` -> the centre 938 px), so SAFE is (391, 40)-(1249, 584) and the phone profile circle is the measured CENTRED box (637, 369, 393) rather than a left-anchored one. Every cover is drawn in a derived `BAND` (408, 48)-(1249, 340), and a `cap` check holds each headline's capital at >= 4 % of the cover height. The red-proof runs on EVERY build (`control_old_window`): it draws the headline where the old 640 x 360 assumption put it, x 328, and the check must reject it -- the phone's crop edge is x 351, so 23 px were cut. Against the three covers as committed at `a76207945e` the new check convicted 3 of 3 (A 23/22 px over the left/right edges, B 63/58 px plus 1017 content pixels under the phone circle, C 63/82 px plus 1778). The profile pictures are untouched -- sha256 identical before and after. | **VERIFY -- rebuilt on `main` 2026-10-09; NOT proven until the operator looks in the Facebook app.** The phone geometry is measured on ONE device (Pixel 9 emulated, 412 px, the mobile WEBSITE); the app may crop differently. | — | Operator: upload the rebuilt cover by hand (same option as now unless `out/preview.html` suggests otherwise -- the pictures cannot be set by API, R-914), then open the Page in the Facebook APP on a phone and say whether the headline and „felhom.eu” are whole. Close on that word. If the app still cuts them, re-measure there and move `PHONE_HDR`; the build follows it. If nothing is done: the Page keeps today's cover, which IS cut on phones | CC |
| **R-919** | Business & legal | P3 | **On a phone the Facebook Page cuts the left edge of the cover: the „s” of „saját szabályaid” and the „f” of „felhom.eu” are gone.** MEASURED 2026-10-08 on the live Page in Chrome DevTools device mode, Pixel 9 (412 × 924, mobile user agent, after a reload so Facebook serves the mobile bundle): `audits/facebook-page-setup-2026-10-08/C2-phone-headline-cut-closeup.png`. The mobile Page header is **412 × 274 = 1,504:1**, so Facebook keeps our cover's full height and shows only the centre **938 px of its 1640 px width (57,2 %)** — **351 px cut from each side**. `marketing/facebook/build.py` builds to a safe area of the centre **1028 × 544** (306 px clear of each edge), which is **45 px wider per side than the phone actually shows**; the light text in `out/cover-c.png` runs from x 333 to x 997, and the left crop edge is x 351, so the first **18 px** of the text are cut. Covers A and B are built from the same safe area and will have the same edge. Two further facts this measurement establishes: **Meta's own help page is wrong about its own rendering** — it states the mobile cover is 2,4:1 where the Page header measures 1,504:1 — and the mobile profile circle is far bigger than assumed (172 px, centred, overlapping the bottom 112 px of the 274 px cover, i.e. source x 637–1030 × y 369–624 is hidden). Not re-cropped on Facebook, per the task's fence. **-- 2026-10-09, FIXED in the build:** `marketing/facebook/build.py` now takes the phone view from the measurement (`PHONE_HDR = (412, 274)` -> the centre 938 px), so SAFE is (391, 40)-(1249, 584) and the phone profile circle is the measured CENTRED box (637, 369, 393) rather than a left-anchored one. Every cover is drawn in a derived `BAND` (408, 48)-(1249, 340), and a `cap` check holds each headline's capital at >= 4 % of the cover height. The red-proof runs on EVERY build (`control_old_window`): it draws the headline where the old 640 x 360 assumption put it, x 328, and the check must reject it -- the phone's crop edge is x 351, so 23 px were cut. Against the three covers as committed at `a76207945e` the new check convicted 3 of 3 (A 23/22 px over the left/right edges, B 63/58 px plus 1017 content pixels under the phone circle, C 63/82 px plus 1778). The profile pictures are untouched -- sha256 identical before and after. **-- 2026-10-09 (operator refinements, same day):** the profile picture now carries the logo MARK only (the lettering was unreadable at 176 px; the mark grew 69 % -> 76 % of the circle, canvas 932 -> 648), and the covers set the headline the way `site.css` sets `.page-index .hero-text h1` (Bold 700, letter-spacing -0.03em, not ExtraBold 800 untracked) with „felhom.eu” drawn from the logo's OWN lettering instead of typed. No geometry changed; every R-919 check and the red-proof stand. | **VERIFY -- rebuilt on `main` 2026-10-09; NOT proven until the operator looks in the Facebook app.** The phone geometry is measured on ONE device (Pixel 9 emulated, 412 px, the mobile WEBSITE); the app may crop differently. | — | Operator: upload the rebuilt cover by hand (same option as now unless `out/preview.html` suggests otherwise -- the pictures cannot be set by API, R-914), then open the Page in the Facebook APP on a phone and say whether the headline and „felhom.eu” are whole. Close on that word. If the app still cuts them, re-measure there and move `PHONE_HDR`; the build follows it. If nothing is done: the Page keeps today's cover, which IS cut on phones | CC |
## Process & tooling — 23 rows (P3 3, P4 20)
+34
View File
@@ -1,5 +1,39 @@
# marketing — CHANGELOG
## facebook — the brand lockup: the mark alone on the profile, the real lettering on the covers (2026-10-09)
Operator refinements on the same day, after looking at the rebuilt pictures. No geometry changed; R-919's
measured constants and all its checks stand.
- **The profile picture is the logo MARK only.** The mark plus the „felhom.eu" lettering left both too small
to read at the 176 px Facebook shows, and smaller than the shape the Page carried before. Dropping the
lettering lets the mark grow from **69 % to 76 %** of the circle, and the canvas drops 932 → **648 px**
(still twice Meta's recommended 320 source, so the floor in `profile_width` went 720 → 640; 720 was above
what the mark alone needs and would have shrunk it for nothing). At 40 px the cloud, the house and the
keyhole now read.
- **Both backgrounds read now.** The preview recommended dark because the *lettering* faded on white — a
reason that no longer exists. It now says what is actually true: dark matches the covers and the Page as it
stands, white separates the circle from the dark cover, and the operator picks.
- **The covers set the headline the way the WEBSITE sets its h1.** `site.css`
`.page-index .hero-text h1` is `font-weight: 700; letter-spacing: -0.03em`; this file used **ExtraBold (800)
with no tracking**. Now `HEADLINE_WEIGHT = "Bold"` and `HEADLINE_TRACK = -0.03`, with real per-glyph
letter-spacing rather than faked by scaling. Bold is narrower, so the headlines grew: A 57 → 62, B 58 → 63,
C 47 → 50.
- **„felhom.eu" is no longer typed.** It is the logo's **own lettering**, cropped out of `logo.png` at a
measured split (mark y 5–289, blank band y 290–295, wordmark y 296–403) and scaled. That lettering is set
in „M+ 2c" / „Vremena Grotesk", fonts this machine does not have (R-916), so typing it in Plus Jakarta Sans
was a different wordmark. The website hero shows the same `logo.png` on the same dark background, so the
covers now match the page. The `DOMAIN` constant is gone with it.
- **Caught before it shipped:** the preview page's new profile paragraph added a fourth `%d`, and the
argument tuple was left in the old order — so the phone paragraph rendered „the centre **76** px of the
**938**-pixel width, with a profile circle **1640** cover-pixels across". All three numbers were real
numbers from the file, which is why it read as plausible rather than as a crash. Fixed and both paragraphs
re-read.
- Checks unchanged and all passing; the cap check now measures the weight the headline is actually set in.
Profile pictures necessarily changed (that was the point), so their sha256 moved: `db05dae5…` dark,
`a44849e4…` white.
## facebook — the covers rebuilt to the phone view we measured (R-919) (2026-10-09)
R-919's fix, from an operator brief (no TASK file in the repo). The covers shipped on 2026-10-08 were cut
+3 -3
View File
@@ -22,7 +22,7 @@ pixel or two differently and rewrites all six files. Measured 2026-10-09. Brand
| File | Where on the Page |
|---|---|
| `out/profile-dark.png` (recommended) or `out/profile-white.png` | Profile picture |
| `out/profile-dark.png` (recommended) or `out/profile-white.png` | Profile picture — the logo **mark only**, no lettering (see below) |
| `out/cover-a.png` / `cover-b.png` / `cover-c.png` — pick one | Cover photo |
| `COPY.md` §1 | Intro / Bemutatkozás (≤ 100 characters) |
| `COPY.md` §2 | **Nowhere today — the field is gone** (R-917). Facebook's current Pages experience has no long-description field: not in the Page's „Névjegy" tab, not in Business Suite's „Oldal módosítása", not in Page settings; the Graph `description` reads `null` and writing it needs `pages_manage_metadata`, which the robot key does not have. Checked 2026-10-08. |
@@ -49,7 +49,7 @@ fasturtle.com) appear **nowhere on Meta's page**; only the 176 px profile size m
assumed, 24 px, is smaller than the ~40 px Meta states.
What we build: the cover at 2 x, **1640 x 624**, well above both stated minimums, as PNG; the profile picture
932 x 932, larger than Meta's 320 x 320 recommendation and therefore downscaled, not enlarged.
648 x 648, twice Meta's 320 x 320 recommendation and therefore downscaled, not enlarged.
**MEASURED on the live Page 2026-10-08, both widths:**
@@ -93,7 +93,7 @@ limit bites.
`website/assets/logo.svg` is **not** usable as a master: its „felhom.eu" lettering is live text in the fonts
„M+ 2c" and „Vremena Grotesk", which DooPlex does not have, so any renderer here draws other letters. The PNG
(645 x 408) is placed unscaled, so the profile picture is 932 x 932 — sharp, not enlarged. A vector master with the
(645 x 408) is placed unscaled. A vector master with the
lettering converted to paths (Inkscape: Path → Object to Path, on the machine that has the fonts) is R-916.
## Cover C
+115 -32
View File
@@ -5,6 +5,14 @@
Run: python3 marketing/facebook/build.py
Exit: 0 every image built and every check passed · 1 a check failed · 2 a render failed
The brand lockup. The headline is set the way the WEBSITE sets its own h1 - `site.css`
`.page-index .hero-text h1` is `font-weight: 700; letter-spacing: -0.03em` - so HEADLINE_WEIGHT is Bold, not
ExtraBold, and HEADLINE_TRACK is -0.03 em. „felhom.eu" is NOT typed in Plus Jakarta Sans: it is the logo's own
lettering, cropped out of logo.png, because that lettering is set in „M+ 2c" / „Vremena Grotesk" (fonts this
machine does not have, R-916) and typing it in another face is a different wordmark. The website hero shows the
same logo.png on the same dark background, so the cover matches the page. The PROFILE picture carries the MARK
only - at 176 px the mark plus the wordmark left both too small to read (operator, 2026-10-09).
Renderer: Pillow only (shapes, text, compositing). Text is set in the website's own Plus Jakarta Sans
(website/assets/fonts/*.woff2), which Pillow reads through FreeType. NOT librsvg: tried 2026-10-08 — pango fell back
to DejaVu for the woff2 fonts, and logo.svg's own lettering is live <text> in 'M+ 2c' / 'Vremena Grotesk', fonts
@@ -63,8 +71,15 @@ WHITE = hexc("#FFFFFF")
L1 = "Saj\u00e1t felh\u0151d,"
L2 = "saj\u00e1t szab\u00e1lyaid"
SLOGAN = L1 + " " + L2
DOMAIN = "felhom.eu"
# No DOMAIN constant: „felhom.eu" is never typed. wordmark() draws the logo's own lettering, so the
# brand name on a cover is the same artwork as on the website, not a look-alike in another face.
# The website's own h1, read from website/assets/site.css (.page-index .hero-text h1).
HEADLINE_WEIGHT = "Bold" # font-weight: 700 - NOT ExtraBold, which is what this file used to use
HEADLINE_TRACK = -0.03 # letter-spacing: -0.03em
# logo.png is one image holding two things: the mark, then a blank band, then the „felhom.eu" lettering.
# MEASURED on the file 2026-10-09: ink runs y 5..289, the band is y 290..295, the wordmark y 296..403.
LOGO_SPLIT_Y = 293
CIRCLE_R = 0.42 # brief §7 A: the check radius, as a fraction of the width
LOGO_R = 0.40 # where the logo's farthest pixel is placed (inside the check radius)
CW, CH = 1640, 624 # cover, 2 x the 820 x 312 computer display
@@ -144,26 +159,64 @@ def text_width(text, size, weight):
return sum(font(size, weight, e).getlength(s) for e, s in runs(text))
def draw_text(img, x, y, text, size, weight, fill, align="left"):
"""Draw at baseline y; x is the left edge, or the centre with align='center'. Returns the width."""
w = text_width(text, size, weight)
def text_width_tracked(text, size, weight, track=0.0):
"""Width with letter-spacing, the way the site puts -0.03em on its h1. `track` is in pixels."""
if not track:
return text_width(text, size, weight)
w = 0.0
for e, s in runs(text):
f = font(size, weight, e)
for ch in s:
w += f.getlength(ch) + track
return w - track
def draw_text(img, x, y, text, size, weight, fill, align="left", track=0.0):
"""Draw at baseline y; x is the left edge, or the centre with align='center'. Returns the width.
With `track` the glyphs are placed one at a time, so letter-spacing is real and not faked by scaling."""
w = text_width_tracked(text, size, weight, track)
if align == "center":
x -= w / 2.0
d = ImageDraw.Draw(img)
for e, s in runs(text):
f = font(size, weight, e)
d.text((x, y), s, font=f, fill=fill, anchor="ls")
x += f.getlength(s)
if track:
for ch in s:
d.text((x, y), ch, font=f, fill=fill, anchor="ls")
x += f.getlength(ch) + track
else:
d.text((x, y), s, font=f, fill=fill, anchor="ls")
x += f.getlength(s)
return w
def fit_size(text, weight, max_w, start):
def fit_size(text, weight, max_w, start, track_em=0.0):
"""The largest size at or below `start` whose tracked width fits max_w."""
size = start
while text_width(text, size, weight) > max_w:
while text_width_tracked(text, size, weight, track_em * size) > max_w:
size -= 1
return size
def headline(c, x, y, text, size, fill, align="left"):
"""One run of headline text in the website's weight and tracking. Returns the PEN x after it, so the
second, blue half of the line starts exactly where the first ended - tracking makes `x + width` wrong
by one letter-space."""
tr = HEADLINE_TRACK * size
w = draw_text(c, x, y, text, size, HEADLINE_WEIGHT, fill, align=align, track=tr)
if align == "center":
x -= w / 2.0
return x + w + tr
def headline_fit(text, max_w, start):
return fit_size(text, HEADLINE_WEIGHT, max_w, start, HEADLINE_TRACK)
def headline_width(text, size):
return text_width_tracked(text, size, HEADLINE_WEIGHT, HEADLINE_TRACK * size)
# ------------------------------------------------------------------ the logo
def alpha_points(img, thr=16):
@@ -188,6 +241,31 @@ def outside_circle(img, bg, r_frac):
return n
_logo_parts = []
def logo_parts():
"""logo.png split into (mark, wordmark), each tightened to its own ink. The split row is MEASURED."""
if _logo_parts:
return _logo_parts[0]
src = Image.open(LOGO_PNG).convert("RGBA")
out = []
for box in ((0, 0, src.width, LOGO_SPLIT_Y), (0, LOGO_SPLIT_Y, src.width, src.height)):
part = src.crop(box)
out.append(part.crop(part.getchannel("A").point(lambda v: 255 if v > 16 else 0).getbbox()))
_logo_parts.append((out[0], out[1]))
return _logo_parts[0]
def wordmark(c, x, y, height, align="left"):
"""Draw „felhom.eu" as the logo's own lettering, scaled to `height`. Returns its width."""
art = logo_parts()[1]
w = int(round(art.width * float(height) / art.height))
art = art.resize((w, int(height)), Image.LANCZOS)
c.alpha_composite(art, (int(round(x - w / 2.0)) if align == "center" else int(round(x)), int(round(y))))
return w
def today_shape():
"""The Page today: logo.png at full size, unscaled, centred on a white square of its own width."""
src = Image.open(LOGO_PNG).convert("RGBA")
@@ -198,18 +276,21 @@ def today_shape():
def profile_width():
"""The square size at which logo.png fits radius LOGO_R x width UNSCALED (never upsampled), at least 720."""
src = Image.open(LOGO_PNG).convert("RGBA")
"""The square size at which the MARK fits radius LOGO_R x width UNSCALED (never upsampled).
Floor 640 = twice the 320 x 320 Meta recommends as a source, so there is resolution to spare at the
176 / 196 px Facebook actually shows. It used to be 720, which was above what the mark alone needs and
would have shrunk the mark inside the circle for no gain."""
src = logo_parts()[0]
pts = alpha_points(src)
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0
dmax = max(math.hypot(x - cx, y - cy) for x, y in pts) + 1
return max(720, int(math.ceil(dmax / LOGO_R / 2.0)) * 2), (cx, cy)
return max(640, int(math.ceil(dmax / LOGO_R / 2.0)) * 2), (cx, cy)
def build_profile(bg, name):
w, (cx, cy) = profile_width()
logo = Image.open(LOGO_PNG).convert("RGBA")
logo = logo_parts()[0]
img = Image.new("RGBA", (w, w), bg)
img.alpha_composite(logo, (int(round(w / 2.0 - cx)), int(round(w / 2.0 - cy))))
path = os.path.join(OUT, name)
@@ -243,13 +324,11 @@ def content_a():
"""Calm: the one line, centred in the band, with the domain under it."""
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
mid = (BAND[0] + BAND[2]) / 2.0
size = fit_size(SLOGAN, "ExtraBold", (BAND[2] - BAND[0]) - 20, 86)
w1 = text_width(L1 + " ", size, "ExtraBold")
w = text_width(SLOGAN, size, "ExtraBold")
x0 = mid - w / 2.0
draw_text(c, x0, 222, L1 + " ", size, "ExtraBold", TEXT1)
draw_text(c, x0 + w1, 222, L2, size, "ExtraBold", BLUE_BRIGHT)
draw_text(c, mid, 284, DOMAIN, 34, "SemiBold", TEXT2, align="center")
size = headline_fit(SLOGAN, (BAND[2] - BAND[0]) - 20, 86)
x = mid - headline_width(SLOGAN, size) / 2.0
x = headline(c, x, 212, L1 + " ", size, TEXT1)
headline(c, x, 212, L2, size, BLUE_BRIGHT)
wordmark(c, mid, 246, round(size * 0.95), align="center")
return c, size
@@ -296,10 +375,10 @@ def content_b():
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = BAND[0] + 12
size = fit_size(L2, "ExtraBold", 456, 64) # the text column; the motif gets the rest
draw_text(c, tx, 170, L1, size, "ExtraBold", TEXT1)
draw_text(c, tx, 170 + size + 14, L2, size, "ExtraBold", BLUE_BRIGHT)
draw_text(c, tx, 170 + size + 14 + 46, DOMAIN, 30, "SemiBold", TEXT2)
size = headline_fit(L2, 456, 64) # the text column; the motif gets the rest
headline(c, tx, 170, L1, size, TEXT1)
headline(c, tx, 170 + size + 14, L2, size, BLUE_BRIGHT)
wordmark(c, tx, 170 + size + 30, round(size * 0.9))
# the home with its server, on the right, scaled to the band's height
hx, hy, hw = 1000, 190, 150 # house body: left, top, width
tiles = [(935, 95, "photo"), (1065, 84, "doc"), (1195, 140, "play"), (1200, 278, "lock"),
@@ -321,10 +400,10 @@ def content_c():
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = BAND[0] + 12
size = fit_size(L2, "ExtraBold", 366, 56)
draw_text(c, tx, 180, L1, size, "ExtraBold", TEXT1)
draw_text(c, tx, 180 + size + 12, L2, size, "ExtraBold", BLUE_BRIGHT)
draw_text(c, tx, 180 + size + 12 + 42, DOMAIN, 30, "SemiBold", TEXT2)
size = headline_fit(L2, 366, 56)
headline(c, tx, 180, L1, size, TEXT1)
headline(c, tx, 180 + size + 12, L2, size, BLUE_BRIGHT)
wordmark(c, tx, 180 + size + 28, round(size * 0.9))
shot = Image.open(SHOT).convert("RGBA")
# The domain chip next to the title names the demo box; covered with the page colour (copy only, source unchanged).
ImageDraw.Draw(shot).rectangle((684, 32, 826, 70), fill=hexc("#0B1322"))
@@ -342,7 +421,7 @@ def content_c():
def cap_height(size):
"""The height of a capital letter at this size, read from the font rather than assumed."""
bb = font(size, "ExtraBold", 0).getbbox("S")
bb = font(size, HEADLINE_WEIGHT, 0).getbbox("S")
return bb[3] - bb[1]
@@ -394,7 +473,7 @@ def preview(profiles, covers):
pct = lambda v, tot: "%.3f%%" % (100.0 * v / tot) # noqa: E731
prof_html = "".join(
'<figure><div class="row"><img class="pc" src="%s" alt=""><img class="pc sm" src="%s" alt="">'
'<img class="pc xs" src="%s" alt=""></div><figcaption>%s — 176 px, 96 px, 40 px</figcaption></figure>'
'<img class="pc xs" src="%s" alt=""></div><figcaption>%s — 176 px, 96 px, 40 px (the mark only)</figcaption></figure>'
% (b64(p), b64(p), b64(p), label) for label, p in profiles)
cov = []
rec = profiles[0][1]
@@ -437,12 +516,16 @@ main{max-width:860px;margin:0 auto}h1{font-size:22px;margin:0 0 4px}h2{font-size
<p class="k">The phone view below is MEASURED on the live Page (Pixel 9 emulated, mobile website, 2026-10-08,
R-919): Facebook keeps the full height and shows only the centre %d px of the %d-pixel width, with a profile
circle over the bottom middle %d cover-pixels across. It is NOT measured in the Facebook app \u2014 check there too.</p>
<p class="k">Profile picture: the DARK background is recommended. The logo's lettering is white with a thin navy edge,
so on white it fades at 40 px; on dark it reads solid, as on the website. The covers below show the dark one.</p>
<p class="k">Profile picture: the logo MARK only \u2014 no \u201efelhom.eu\u201d lettering. At the 176 px Facebook shows (and
40 px in a comment) the mark plus the lettering left both too small to read, so the lettering was dropped and the
mark grew to %d %% of the circle. BOTH backgrounds now read at 40 px, which was not true while the lettering was
there: dark matches the covers and the Page as it stands, white separates the circle from the dark cover. The
covers below show the dark one.</p>
<h2>Profile picture</h2>%s
%s
</main></body></html>
""" % (round(CW_PHONE), CW, round(PHONE_CIRCLE[2]), prof_html, "".join(cov))
""" % (round(CW_PHONE), CW, round(PHONE_CIRCLE[2]),
round(100.0 * logo_parts()[0].width / profile_width()[0]), prof_html, "".join(cov))
path = os.path.join(OUT, "preview.html")
with open(path, "w", encoding="utf-8") as fh:
fh.write(html)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 105 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 27 KiB