marketing/facebook: covers rebuilt to the MEASURED phone view (R-919 -> VERIFY)
gates / gates (push) Successful in 4m28s

build.py believed a phone shows 640x360 of the cover. It shows 412x274 =
1.504:1 -- the full height and only the centre 938 px of 1640, 351 px off each
side. PHONE_HDR = (412, 274) now drives CW_PHONE, so SAFE is (391,40)-(1249,584),
858 px wide with a 40 px margin inside each crop edge.

QUIET is no longer one formula for both circles: the computer circle is
left-anchored, the phone circle is CENTRED and hides only its measured box
(637,369,393). The old shared formula, applied to a centred circle, would have
blanked everything from x 0 to x 1054 below y 345.

Every cover draws in a derived BAND (408,48)-(1249,340), and a cap check holds
each headline's capital at >= 4% of the cover height; A/B/C come out at 45, 45
and 37 px against a 25 px floor.

RED-PROOF, and it is permanent: control_old_window() runs on every build and
draws the headline where the old assumption put it (x 328); the check must
reject it. The phone's crop edge is x 351, so 23 px were cut; the measured safe
edge is x 391. Against the covers as committed at a76207945e the new check
convicted 3 of 3 -- A 23/22 px past the left/right edges, B 63/58 px plus 1017
content pixels under the phone circle, C 63/82 px plus 1778.

Covers redrawn inside the band: A one centred line; B the home motif scaled
with two tiles moved into the lower-right strip, the one area below the band a
phone still shows beside the profile circle; C the dashboard screen 470 -> 370
px so frame and base fit. preview.html shows the phone panel at the measured
938x624 with the centred circle and no longer claims 640x360.

Profile pictures untouched -- sha256 identical before and after, and not in the
diff. Built on DooPlex: the PNGs are byte-reproducible there (Pillow 11.1.0) and
not across Pillow versions; the README now says so.

R-919 is VERIFY, not closed: the geometry is measured on ONE emulated device in
the mobile website, so the operator's check in the Facebook app is the
acceptance. Nothing uploaded, no Graph API call, FACEBOOK_API never read.
This commit is contained in:
2026-10-09 06:59:53 +02:00
parent a76207945e
commit 796a9fdf40
9 changed files with 347 additions and 71 deletions
+147
View File
@@ -0,0 +1,147 @@
# REPORT — R-919: the Facebook cover rebuilt to the measured phone view (2026-10-09)
A `REPORT-<topic>.md` sibling; the shared `REPORT.md` was not touched. Evidence for the measurement it
builds on: `documentation/audits/facebook-page-setup-2026-10-08/`.
## 1. Baseline and commits
| | |
|---|---|
| Baseline | `felhom.eu` `main` = `a76207945e`, clean on the Windows tree and on DooPlex |
| Pushed | see §9 |
| Repos touched | `felhom.eu` only — `marketing/facebook/`, `marketing/CHANGELOG.md`, `documentation/backlog/OPEN-ITEMS.md`, this report |
| Built on | **DooPlex** (Pillow 11.1.0), not the workstation — see §4 |
## 2. Scenario A — the red-proof, with its numbers
The measured geometry was run against the three covers **as committed at `a76207945e`**, by importing
`build.py` unchanged and overriding only the constants, so the content was today's. **All three were
convicted.** Phone window: the centre **938,3 px** of 1640 (x 351 … 1289). New `SAFE` (391, 40, 1249, 584).
New phone `QUIET` (613, 345, 1054, 624).
| cover | content box | past the left edge | past the right edge | content pixels under the phone circle |
|---|---|---|---|---|
| cover-a.png | (368, 184, 1271, 303) | 368 < 391 — **23 px** | 1271 > 1249 — **22 px** | 0 |
| cover-b.png | (328, 44, 1307, 567) | 328 < 391 — **63 px** | 1307 > 1249 — **58 px** | **1017** |
| cover-c.png | (328, 108, 1331, 455) | 328 < 391 — **63 px** | 1331 > 1249 — **82 px** | **1778** |
Those content boxes are identical to the ones the unmodified build prints for the committed covers, which
is the control that the red-proof really did run against today's content and not against a redrawn one.
**The red-proof is now permanent, not a one-off.** `control_old_window()` runs on **every build**, beside the
circle check's own control: it draws the headline where the old 640 × 360 assumption put it (**x 328**) and
the safe check must reject it. The build prints the two numbers that differ, because they are easy to
confuse: the phone's crop edge is **x 351**, so **23 px of the headline were actually cut**; the measured
safe edge is **x 391**, 63 px further in, which is the 40 px `MARGIN` on top of the crop.
## 3. Scenario B — the rebuilt covers
Canonical build (DooPlex). `BAND` = (408, 48, 1249, 340) — inside `SAFE`, right of the computer circle,
above the phone circle — derived from the constants, so a re-measurement moves the designs with it.
| cover | content box | headline size → capital height | under the computer circle | under the phone circle |
|---|---|---|---|---|
| cover-a.png | (424, 172, 1231, 287) | 57 → **45 px** | 0 | 0 |
| cover-b.png | (420, 52, 1235, 575) | 58 → **45 px** | 0 | 0 |
| cover-c.png | (420, 52, 1243, 339) | 47 → **37 px** | 0 | 0 |
`CAP_MIN` is 25 px (4 % of 624); the smallest headline is 37 px, about 48 % clear of the rule. Measured
clearance inside the phone's own crop edges: **69–73 px on the left, 49–57 px on the right** — the 40 px
margin plus the layout's own air.
What changed in each design:
- **A — calm.** One centred line, now fitted to the band rather than the old wide safe area, with the domain
under it. Nothing else.
- **B — the idea.** Headline left; the home, its server and its app tiles right, scaled to the band. Two of
the seven tiles sit in the **lower-right strip** (x ≥ 1054), which is the one area below the band that a
phone still shows beside the profile circle — so the composition keeps its lower half instead of ending at
y 340.
- **C — the product.** Headline left; the dashboard right, its screen **470 → 370 px wide** so the frame and
its base fit between the text and the right crop edge. The screenshot's text was never legible at cover
size (it renders ~280 px wide on a 1250 px Facebook cover, and did before at ~358 px); it still reads as a
real product dashboard, with the sidebar, the tables and the status badges visible. **It was not dropped**
— §8's fallback — but the operator picks, and `out/preview.html` shows all three.
The computer view was checked by eye on the rendered PNGs as well as by the numbers: the band sits in the
cover's upper half by necessity (the phone hides the bottom middle), so each cover's glow was moved down to
carry the lower half instead of leaving it flat.
## 4. Profile pictures — untouched, proven
```
before 936cb0386c8fb46c6f9f7ba7ac97b91c1d89fafef05d86f6a7ad9a4d951ec471 profile-dark.png
f528e0820ff4e527e03d4a0a909ae74ca09d715d0ae4faff89cd3c01ea4fc5b7 profile-white.png
after 936cb0386c8fb46c6f9f7ba7ac97b91c1d89fafef05d86f6a7ad9a4d951ec471 profile-dark.png
f528e0820ff4e527e03d4a0a909ae74ca09d715d0ae4faff89cd3c01ea4fc5b7 profile-white.png
```
Identical, and neither file appears in the diff. **This only holds because the build ran on DooPlex.** The
workstation has Pillow 12.3.0; DooPlex has 11.1.0, and the two render the same text a pixel or two
differently — a rebuild on Windows rewrites **all six** PNGs, profile pictures included, and shifts the
measured content boxes (cover-a's right edge 1235 vs 1231, cover-c's fitted headline 45 vs 47). DooPlex
reproduces the committed bytes exactly (a rebuild there leaves `git status` clean), so it is the build
machine; the README now says so. Pillow was installed on the workstation for this work and used only to
iterate on the design, never for the committed output.
## 5. The rebuilt files
| file | size | Gitea |
|---|---|---|
| `marketing/facebook/out/cover-a.png` | 1640 × 624, 59 KB | `gitea.dooplex.hu/admin/felhom.eu/src/branch/main/marketing/facebook/out/cover-a.png` |
| `marketing/facebook/out/cover-b.png` | 1640 × 624, 54 KB | `…/marketing/facebook/out/cover-b.png` |
| `marketing/facebook/out/cover-c.png` | 1640 × 624, 97 KB | `…/marketing/facebook/out/cover-c.png` |
| `marketing/facebook/out/preview.html` | 1295 KB, self-contained | `…/marketing/facebook/out/preview.html` |
Each PNG's size was read back from the file by the build, not taken from the constants.
## 6. What the operator does, in plain words
1. **Upload the new cover.** Open `out/preview.html` (download it from Gitea; it opens on its own), pick a
cover — the same option as now unless the preview gives you a reason to change — and upload it to the
Page by hand, the way you did last time. About a minute. The pictures cannot be set by robot (R-914).
2. **Then check it on your phone, in the Facebook app.** Open the Page and look at the cover: is the second
line „saját szabályaid" whole, and is „felhom.eu" whole? **This is the real test.** What was measured was
the phone **website** in a simulator, on one device. If the app cuts it differently, say so and the
numbers move — the build follows them.
Nothing else changed: the profile picture, the texts, the button and the Page name are as they were.
## 7. Register
Rows before **140**, after **140**, opened **0**, closed **0** — counted the way `register_shape_gate.py`
counts. **R-919 moved OPEN → VERIFY** and stays in `OPEN-ITEMS.md` deliberately: the build is fixed, the
*result* is not proven until the operator looks in the Facebook app, so it is not a finished row and does
not belong in `CLOSED-ITEMS.md` yet. Its What now carries the fix and the red-proof numbers; its Next action
is the operator's two steps and what to do if the app still cuts the cover.
## 8. Observations
- **The computer profile circle no longer overlaps the cover at all** (measured 2026-10-08: the cover ends
at y 531 and the circle starts at y 547). `DESK_CIRCLE` still reserves the bottom-left corner as if it
did. **NOT-A-FINDING: deliberate.** The brief said to keep the computer constants unchanged, and the
reservation is conservative in the safe direction; it costs only a corner the designs do not use. It is
now written down as a conservatism rather than as a fact, in the constant's own comment.
- **`QUIET` used one formula for both circles, and that formula was only right for a left-anchored one.**
Applied to the measured centred phone circle it would have blanked everything from x 0 to x 1054 below
y 345 — about two-thirds of the cover's lower half, for no reason. **NOT-A-FINDING in the register: fixed
in this session** (the size rule), and it never shipped, because the phone circle was wrong anyway.
- **The build is not byte-reproducible across Pillow versions.** See §4. **NOT-A-FINDING: not a defect**, and
it costs nothing once the build machine is named — which the README now does. It would become a finding
only if the build moved into CI on a different image.
- **The preview page claimed a phone shows 640 × 360** and drew its phone panel at that aspect. It now takes
its shape, its circle and its caption from the measured constants and says MEASURED, with the source-pixel
and phone-pixel diameters both spelled out (393 cover-pixels, 173 px on a 412 px screen) so neither can be
read as the other. **NOT-A-FINDING: fixed in this session**, part of the same change.
- **Nothing was uploaded, no Graph API call was made, no browser touched Facebook, and `FACEBOOK_API` was
never read.**
## 9. Gates, commit, CI
Filled in below after the run.
## 10. Teardown
Nothing on any host, the hub or Facebook. The work ran in the two git working trees only; the throwaway
red-proof and phone-simulation scripts live in this session's scratchpad and `/tmp` on DooPlex, and the
DooPlex copy was removed. No guest, no container, no service was started or stopped.
+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. | **OPEN — owner: CC** | — | Narrow `build.py`'s safe area to the centre ≤ 938 px at full height (use ~900 px for margin) and widen the phone profile-circle exclusion to the measured 172 px centred shape, with the check shown failing on today's covers first; rebuild A/B/C; the operator re-uploads the chosen cover by hand. Until then the cover reads „aját szabályaid” / „elhom.eu” on every phone | 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. | **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)
+32
View File
@@ -1,5 +1,37 @@
# marketing — CHANGELOG
## 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
on every phone; this rebuilds them to the measured geometry. Report: `REPORT-facebook-cover-r919.md`.
- **The geometry is measured now, not assumed.** `build.py` believed a phone shows 640 x 360 of the cover.
It shows **412 x 274 = 1,504 : 1** — the full height and only the **centre 938 px of 1640**, 351 px off
each side. `PHONE_HDR = (412, 274)` drives `CW_PHONE`, so `SAFE` is now **(391, 40)–(1249, 584)**, 858 px
wide with a 40 px margin inside each crop edge.
- **`QUIET` is no longer one formula for both circles.** The computer circle is *left-anchored* (everything
to its left is under it); the phone circle is **centred** and hides only its own measured box
`(637, 369, 393)`. The old shared formula would have blanked the whole left half of the cover's bottom.
- **A derived `BAND`** — inside SAFE, right of the computer circle, above the phone circle — is where every
cover draws, so a re-measurement moves the three designs with it.
- **A `cap` check** holds each headline's capital at ≥ 4 % of the cover height (25 px). A, B, C come out at
45, 45 and 37 px.
- **The red-proof runs on every build**, beside the circle check's own control: `control_old_window` draws
the headline where the old assumption put it (x 328) and the check must reject it. Against the covers as
committed at `a76207945e` the new check convicted **3 of 3** — A 23 px / 22 px past the left and right
edges, B 63 / 58 px plus 1017 content pixels under the phone circle, C 63 / 82 px plus 1778.
- **The three covers were redrawn** inside the band: A one centred line; B the headline left with the home,
its server and now seven app tiles right — two of them in the lower-right strip, the one place below the
band a phone still shows beside the profile circle; C the headline left with the dashboard, its screen
470 → 370 px wide to fit. `out/preview.html` shows the phone panel at the measured 938 x 624 with the
centred circle, labelled MEASURED, and no longer claims 640 x 360.
- **The profile pictures are untouched** — sha256 identical before and after, and they are not in the diff.
- **Build it on DooPlex.** The PNGs are byte-reproducible there (Pillow 11.1.0); Pillow 12.3.0 on the Windows
workstation re-renders text a pixel or two differently and rewrites all six files. Noted in the README.
- Nothing was uploaded to Facebook and no key was read. **R-919 is VERIFY, not closed:** the geometry is
measured on one emulated device in the mobile website, so the operator's check in the Facebook **app** is
the acceptance step.
## facebook — the Page's settings set by hand, and Meta's size page read first-hand (2026-10-08, evening)
Ran `marketing/facebook/TASK-page-setup-windows.md` from the Windows workstation through Claude in Chrome.
+34 -14
View File
@@ -10,8 +10,12 @@ python3 marketing/facebook/build.py
One command makes every PNG and `out/preview.html`, and runs the checks: the profile circle (no logo pixel
outside 0.42 x width; shown failing first on today's shape), the cover safe area (content inside what computer
AND phone show, clear of the profile circle), and the sizes read back from the files. Exit 0 = all passed.
Needs Python 3 + Pillow only. Brand source: `website/assets/logo.png` (the logo master — see below),
AND phone show, clear of both profile circles), the headline's capital height, and the sizes read back from the
files. Exit 0 = all passed. Needs Python 3 + Pillow only.
**Build it on DooPlex.** The PNGs are byte-reproducible there (Pillow 11.1.0 — a rebuild leaves `git status`
clean) and NOT across Pillow versions: Pillow 12.3.0 on the Windows workstation re-renders the same text a
pixel or two differently and rewrites all six files. Measured 2026-10-09. Brand source: `website/assets/logo.png` (the logo master — see below),
`website/assets/site.css` tokens, `website/assets/fonts/` (Plus Jakarta Sans).
## What goes where
@@ -51,19 +55,35 @@ What we build: the cover at 2 x, **1640 x 624**, well above both stated minimums
- **Computer (1435 px viewport): fine.** The cover renders whole — no crop, no distortion — the cover container
caps at 1250 px, and the profile circle sits 16 px *below* the cover, so it covers no cover text.
- **Phone (emulated Pixel 9, 412 x 924): the cover is CUT — R-919.** The mobile Page header is
**412 x 274 = 1,504:1**, so Facebook keeps the full height and shows only the **centre 938 px of the 1640 px
width (57,2 %)** — **351 px cut from each side**. The safe area below is the centre 1028 x 544, i.e. 306 px
clear of each edge, which is **45 px per side too wide**. In `out/cover-c.png` the light text runs x 333 to
x 997 against a left crop edge of x 351, so **18 px of it are cut**: the headline reads „aját szabályaid" and
the wordmark „elhom.eu" on every phone. The mobile profile circle is **172 px and centred**, hiding source
x 637–1030 x y 369–624 — part of the dashboard picture, no text.
- **Phone (emulated Pixel 9, 412 x 924): the cover WAS cut — R-919, fixed 2026-10-09.** The mobile Page header
is **412 x 274 = 1,504:1**, so Facebook keeps the full height and shows only the **centre 938 px of the
1640 px width (57,2 %)** — **351 px cut from each side**. The old safe area was the centre 1028 x 544, i.e.
306 px clear of each edge: **45 px per side too wide**. The light text of the old `cover-c.png` ran from
x 333 against a left crop edge of x 351, so **18 px of it were cut** — the headline read „aját szabályaid"
and the wordmark „elhom.eu" on every phone. The mobile profile circle is **172 px and centred**, hiding
source x 637–1030 x y 369–624.
**So `build.py`'s safe area is wrong for phones** and needs narrowing to the centre **≤ 938 px at full height**
(use ~900 px for margin), with the phone profile-circle exclusion widened to the measured centred 172 px shape —
shown failing on today's covers first. **Meta's stated mobile ratio of 2,4:1 is not what the Page header does**
(1,504:1 measured), which is why a figure from Meta's page is not a substitute for looking at the Page. Getting
Facebook's phone layout at all needs DevTools device mode **plus a reload**; the audit's README has the recipe.
### What the build does about it now (2026-10-09)
`build.py` takes the phone view from the measurement, not from Meta: `PHONE_HDR = (412, 274)` gives
`CW_PHONE` = 938 px, so **SAFE is the centre (391, 40)–(1249, 584)** — 858 px wide, MARGIN 40 px inside each
crop edge. The phone profile circle is the measured **centred** box `(637, 369, 393)`, and `QUIET` is no longer
one formula for both circles: the computer one is *left-anchored* (everything to its left is under it), the
phone one hides **only its own box**. Every cover is drawn inside `BAND` — inside SAFE, right of the computer
circle, above the phone circle — which is derived from those constants, so a re-measurement moves the designs
with it. A `cap` check keeps each headline's capital at **≥ 4 % of the cover height** (25 px), so the text
survives the phone shrinking the cover to 412 px.
**The check was shown failing first.** Against the covers as committed at `a76207945e`: cover A was 23 px over
on the left and 22 px on the right, cover B 63 px / 58 px with 1017 content pixels under the phone circle, and
cover C 63 px / 82 px with 1778 under it — 3 of 3 convicted.
**Grade of the phone figures: MEASURED, but on ONE device** (Pixel 9 emulated, 412 px, the mobile **website**).
Not measured in the Facebook app and not at any other width — which is why MARGIN is 40 px and why the
operator's check in the app is the acceptance step. **Meta's stated mobile ratio of 2,4:1 is not what the Page
header does** (1,504:1 measured), which is why a figure from Meta's page is no substitute for looking at the
Page. Getting Facebook's phone layout at all needs DevTools device mode **plus a reload**; the audit's README
has the recipe.
Page `about` = 100 characters: developers.facebook.com/docs/graph-api/reference/page/ (Meta, first-hand). The
Page's own „Bemutatkozás" editor counts to **255** (seen on screen 2026-10-08); §1 is 99 characters, so neither
+128 -54
View File
@@ -15,14 +15,18 @@ Checks (each prints its numbers; a failure exits 1):
circle every logo pixel of a profile picture lies inside radius 0.42 x width of the centre (8 % margin).
Shown failing first on the shape the Page has today (logo.png, full size, on a square).
safe every pixel of a cover's CONTENT layer (text, logo, drawings — not the background) lies inside the
area both desktop and phone show, and outside the places the profile circle covers.
area both desktop and phone show, and outside the places the profile circle covers. Shown failing
first on the covers as committed at a76207945e — all three were cut on phones (R-919).
cap a cover's headline is set at a capital-letter height of at least CAP_MIN, so it stays readable
once the phone has shrunk the cover to 412 px wide.
size each PNG's size is read back from the file, never taken from the constants here.
Sizes (graded READ, second-hand — Meta's own help page needs a browser; see README.md for the URLs):
cover shown 820 x 312 on computers, 640 x 360 on phones; built at 2 x = 1640 x 624.
profile shown 176 px (computer) / 196 px (phone, older layouts) — built at the size where logo.png fits unscaled.
profile circle over the cover: computer 16 px from the left, 176 px from the top (176 px circle);
phone 24 px from the left, 196 px from the top (140 px circle).
Sizes. The COMPUTER figures are graded READ, second-hand (820 x 312; built at 2 x = 1640 x 624; the
profile circle 16 px from the left, 176 px from the top, 176 px across). The PHONE figures are graded
MEASURED — see the constants block: one device (Pixel 9 emulated, 412 x 924, the mobile WEBSITE), NOT the
Facebook app and no other width. Meta's own help page claims 2,4:1 for the phone cover and is wrong; that
figure is what cut „saját szabályaid" down to „aját szabályaid" on every phone (R-919). The profile picture
is shown 176 px (computer) / 196 px (phone) and is built at the size where logo.png fits unscaled.
"""
import base64
import math
@@ -65,16 +69,41 @@ 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
SCALE = CW / 820.0
# Phone shows the full height at 640:360 → the centre CW_PHONE pixels of the width.
CW_PHONE = CH * 640.0 / 360.0
# --- the phone view: MEASURED, never read off Meta's help page (R-919) -------------------------------
# Measured 2026-10-08 on the live Page in Chrome DevTools device mode, Pixel 9 (412 x 924, mobile user
# agent, AFTER a reload — without one Facebook keeps serving the desktop bundle). The Page's mobile
# header renders 412 x 274 CSS px = 1,504 : 1, so Facebook keeps the cover's FULL HEIGHT and shows only
# the centre CW_PHONE pixels of its width. Evidence and the recipe to re-measure:
# documentation/audits/facebook-page-setup-2026-10-08/ (README.md, C2-phone-headline-cut-closeup.png).
# GRADE: measured on ONE device, in the mobile WEBSITE. NOT measured in the Facebook app and not at any
# other width — which is why MARGIN below is deliberate and why the operator's app check is the
# acceptance step, not this build.
PHONE_HDR = (412.0, 274.0)
CW_PHONE = CH * PHONE_HDR[0] / PHONE_HDR[1] # 938.3 of 1640 — about 351 px lost from EACH side
PHONE_X0 = (CW - CW_PHONE) / 2.0
MARGIN = 40
SAFE = (int(math.ceil(PHONE_X0 + MARGIN)), MARGIN, int(CW - PHONE_X0 - MARGIN), CH - MARGIN)
PAD = 24
DESK_CIRCLE = (16 * SCALE, 176 * SCALE, 176 * SCALE) # x, y, diameter in cover pixels
PHONE_K = CH / 360.0
PHONE_CIRCLE = (PHONE_X0 + 24 * PHONE_K, 196 * PHONE_K, 140 * PHONE_K)
QUIET = [(0, c[1] - PAD, c[0] + c[2] + PAD, CH) for c in (DESK_CIRCLE, PHONE_CIRCLE)]
# x, y, diameter in cover pixels. The COMPUTER circle is UNCHANGED (second-hand, and left-anchored: it
# sits against the cover's left edge, so everything to its left is under it too). On 2026-10-08 the
# computer circle was measured to sit 16 px BELOW the cover and to overlap nothing — this stays as a
# deliberate conservatism, not because it is still true.
DESK_CIRCLE = (16 * SCALE, 176 * SCALE, 176 * SCALE)
# The PHONE circle is MEASURED and CENTRED: 172 display px over the bottom middle of the header, which
# maps to source x 637..1030, y 369..bottom. A centred circle hides its own box only — never the whole
# width to its left, which is why QUIET can no longer be one formula for both.
PHONE_CIRCLE = (637.0, 369.0, 393.0)
QUIET = [(0, DESK_CIRCLE[1] - PAD, DESK_CIRCLE[0] + DESK_CIRCLE[2] + PAD, CH),
(PHONE_CIRCLE[0] - PAD, PHONE_CIRCLE[1] - PAD, PHONE_CIRCLE[0] + PHONE_CIRCLE[2] + PAD, CH)]
# The band every cover draws in: inside SAFE, clear of the computer circle on the left, above the phone
# circle. Derived, so a re-measurement moves the designs with it.
BAND = (max(SAFE[0], int(math.ceil(DESK_CIRCLE[0] + DESK_CIRCLE[2] + PAD))), SAFE[1] + 8,
SAFE[2], int(PHONE_CIRCLE[1] - PAD) - 5)
CAP_MIN = int(round(0.04 * CH)) # a headline capital is at least 4 % of the cover height (25 px at 624)
# What this file believed before R-919: that a phone shows 640 x 360 of the cover. Kept ONLY so the
# control below can convict it - a check that has never been seen failing is a wish, not a check.
OLD_SAFE_X0 = (CW - CH * 640.0 / 360.0) / 2.0 + MARGIN
FAILS = []
@@ -211,16 +240,17 @@ def background(glow_at=(0.78, 0.25)):
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 = (SAFE[0] + SAFE[2]) / 2.0
size = fit_size(SLOGAN, "ExtraBold", (SAFE[2] - SAFE[0]) - 120, 66)
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, 238, L1 + " ", size, "ExtraBold", TEXT1)
draw_text(c, x0 + w1, 238, L2, size, "ExtraBold", BLUE_BRIGHT)
draw_text(c, mid, 300, DOMAIN, 34, "SemiBold", TEXT2, align="center")
return c
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")
return c, size
def rack(d, x, y, w, h, fill, dot):
@@ -260,59 +290,87 @@ def tile(d, cx, cy, s, kind):
def content_b():
"""The idea: the headline on the left, a home with its server and its apps on the right. Both
columns live inside BAND -- the phone hides the cover's bottom middle behind the profile circle,
so nothing is drawn down there any more (R-919)."""
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = SAFE[0] + 24 # text stays above the profile circles (y < 316)
draw_text(c, tx, 170, L1, 64, "ExtraBold", TEXT1)
draw_text(c, tx, 244, L2, 64, "ExtraBold", BLUE_BRIGHT)
draw_text(c, tx, 290, DOMAIN, 32, "SemiBold", TEXT2)
# the home with its server, on the right
hx, hy, hw = 1010, 300, 220 # house body: left, top, width
tiles = [(975, 110, "photo"), (1140, 92, "doc"), (1260, 210, "play"), (1260, 400, "lock"), (970, 520, "folder")]
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)
# 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"),
(928, 262, "folder"), (1155, 425, "photo"), (1110, 540, "doc")]
for x, y, _ in tiles:
d.line((hx + hw / 2, hy + 90, x, y), fill=(46, 168, 245, 110), width=4)
d.rectangle((hx, hy, hx + hw, hy + 190), fill=BG1, outline=BLUE_BRIGHT, width=6)
d.polygon([(hx - 30, hy + 4), (hx + hw / 2, hy - 120), (hx + hw + 30, hy + 4)], fill=BG1, outline=BLUE_BRIGHT)
d.line([(hx - 30, hy + 4), (hx + hw / 2, hy - 120), (hx + hw + 30, hy + 4)], fill=BLUE_BRIGHT, width=6, joint="curve")
rack(d, hx + 40, hy + 40, hw - 80, 110, BLUE, TEXT1)
d.line((hx + hw / 2, hy + 70, x, y), fill=(46, 168, 245, 110), width=4)
d.rectangle((hx, hy, hx + hw, hy + 130), fill=BG1, outline=BLUE_BRIGHT, width=6)
d.polygon([(hx - 30, hy + 4), (hx + hw / 2, hy - 85), (hx + hw + 30, hy + 4)], fill=BG1, outline=BLUE_BRIGHT)
d.line([(hx - 30, hy + 4), (hx + hw / 2, hy - 85), (hx + hw + 30, hy + 4)], fill=BLUE_BRIGHT, width=6, joint="curve")
rack(d, hx + 28, hy + 28, hw - 56, 75, BLUE, TEXT1)
for x, y, kind in tiles:
tile(d, x, y, 92, kind)
return c
tile(d, x, y, 62, kind)
return c, size
def content_c():
"""The product: the headline on the left, the demo box's own dashboard on the right. Both inside
BAND -- the screen used to run past the phone's right crop edge and into the phone circle."""
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = SAFE[0] + 24
draw_text(c, tx, 180, L1, 52, "ExtraBold", TEXT1)
draw_text(c, tx, 246, L2, 52, "ExtraBold", BLUE_BRIGHT)
draw_text(c, tx, 292, DOMAIN, 30, "SemiBold", TEXT2)
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)
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"))
sw = 470
sw = 370
sh = int(round(sw * shot.height / shot.width))
shot = shot.resize((sw, sh), Image.LANCZOS)
fx, fy, b = 800, 110, 14 # frame left/top, bezel
fx, fy, b = 814, 56, 14 # frame left/top, bezel
d.rectangle((fx, fy, fx + sw + 2 * b, fy + sh + 2 * b), fill=BG2, outline=LINE, width=3)
c.alpha_composite(shot, (fx + b, fy + b))
base_y = fy + sh + 2 * b
d.polygon([(fx - 30, base_y + 22), (fx + sw + 2 * b + 30, base_y + 22), (fx + sw + 2 * b, base_y),
(fx, base_y)], fill=LINE)
return c
return c, size
def check_safe(name, content):
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")
return bb[3] - bb[1]
def control_old_window():
"""Draw the headline where the OLD 640 x 360 assumption put it - which is where every shipped cover
had it - and return whether the measured check rejects that. It must. On the live Page the phone cut
18 px off exactly this layout: „saj\u00e1t szab\u00e1lyaid" read „aj\u00e1t szab\u00e1lyaid" (R-919)."""
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
draw_text(c, OLD_SAFE_X0 + 24, 180, L1, 52, "ExtraBold", TEXT1)
x0 = min(p[0] * 4 for p in alpha_points(c.resize((CW // 4, CH // 4)), thr=8))
return x0 < SAFE[0], x0
def check_safe(name, content, headline):
pts = alpha_points(content.resize((CW // 4, CH // 4)), thr=8)
xs = [p[0] * 4 for p in pts]
ys = [p[1] * 4 for p in pts]
bbox = (min(xs), min(ys), max(xs) + 3, max(ys) + 3)
inside = bbox[0] >= SAFE[0] and bbox[1] >= SAFE[1] and bbox[2] <= SAFE[2] and bbox[3] <= SAFE[3]
check(inside, "%s: content box %s inside the safe area %s" % (name, bbox, SAFE))
hits = 0
hits = []
for q in QUIET:
hits += sum(1 for x, y in zip(xs, ys) if q[0] <= x <= q[2] and q[1] <= y <= q[3])
check(hits == 0, "%s: %d content pixels where the profile circle sits (computer or phone)" % (name, hits))
hits.append(sum(1 for x, y in zip(xs, ys) if q[0] <= x <= q[2] and q[1] <= y <= q[3]))
check(sum(hits) == 0, "%s: %d content pixels under the computer circle, %d under the phone circle"
% (name, hits[0], hits[1]))
cap = cap_height(headline)
check(cap >= CAP_MIN, "%s: headline size %d, capital height %d px (at least %d)"
% (name, headline, cap, CAP_MIN))
def build_cover(name, content, glow):
@@ -348,12 +406,17 @@ def preview(profiles, covers):
'style="left:%s;top:%s;width:%s"></div>'
% (src, safe, b64(rec), pct(dk[0], CW), pct(dk[1], CH), pct(dk[2], CW)))
crop_l = PHONE_X0 / CW_PHONE * 100
phone = ('<div class="phone"><div class="crop"><img src="%s" alt="" style="width:%.3f%%;left:-%.3f%%">'
phone = ('<div class="phone" style="aspect-ratio:%.2f/%d"><div class="crop">'
'<img src="%s" alt="" style="width:%.3f%%;left:-%.3f%%">'
'</div><img class="ov" src="%s" alt="" style="left:%s;top:%s;width:%s"></div>'
% (src, CW / CW_PHONE * 100, crop_l, b64(rec), pct(ph[0] - PHONE_X0, CW_PHONE),
pct(ph[1], CH), pct(ph[2], CW_PHONE)))
% (CW_PHONE, CH, src, CW / CW_PHONE * 100, crop_l, b64(rec),
pct(ph[0] - PHONE_X0, CW_PHONE), pct(ph[1], CH), pct(ph[2], CW_PHONE)))
cov.append('<section><h2>%s</h2><p class="k">Computer (820 x 312), dashed = safe area (preview only)</p>%s'
'<p class="k">Phone (640 x 360 crop)</p>%s</section>' % (label, desk, phone))
'<p class="k">Phone \u2014 MEASURED (R-919): the centre %d px of %d at full height, and the '
'profile circle %d cover-pixels across - %d px on a %d px phone screen - centred. '
'NOT 640 x 360, and NOT Meta\u2019s 2,4:1.</p>%s</section>'
% (label, desk, round(CW_PHONE), CW, round(ph[2]),
round(ph[2] * PHONE_HDR[0] / CW_PHONE), round(PHONE_HDR[0]), phone))
html = """<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Facebook Page pictures</title>
@@ -367,16 +430,19 @@ main{max-width:860px;margin:0 auto}h1{font-size:22px;margin:0 0 4px}h2{font-size
.desk{position:relative;max-width:820px;margin-bottom:56px}.desk>img:first-child{width:100%%;display:block}
.ov{position:absolute;border-radius:50%%;border:3px solid #fff;box-sizing:border-box}
.safe{position:absolute;border:2px dashed #E0A93E;box-sizing:border-box;pointer-events:none}
.phone{position:relative;width:min(360px,100%%);aspect-ratio:640/360;margin-bottom:48px}
.phone{position:relative;width:min(412px,100%%);aspect-ratio:938/624;margin-bottom:48px}
.crop{position:absolute;inset:0;overflow:hidden}.crop img{position:absolute;top:0;height:100%%}
</style></head><body><main>
<h1>Felhom.eu - Facebook pictures</h1>
<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>
<h2>Profile picture</h2>%s
%s
</main></body></html>
""" % (prof_html, "".join(cov))
""" % (round(CW_PHONE), CW, round(PHONE_CIRCLE[2]), prof_html, "".join(cov))
path = os.path.join(OUT, "preview.html")
with open(path, "w", encoding="utf-8") as fh:
fh.write(html)
@@ -401,12 +467,20 @@ def main():
n = outside_circle(Image.open(p), bg, CIRCLE_R)
check(n == 0, "%s: %d logo pixels outside the circle" % (name, n))
built.append(p)
print("safe-area check (computer 820x312 + phone 640x360; profile circles kept clear)")
covers = [("cover-a.png", content_a(), (0.78, 0.25)), ("cover-b.png", content_b(), (0.70, 0.45)),
("cover-c.png", content_c(), (0.66, 0.40))]
print("safe-area check (computer %d x %d; the phone shows the centre %d px of %d - MEASURED, R-919)"
% (CW, CH, round(CW_PHONE), CW))
print(" safe %s, band %s, both profile circles kept clear" % (SAFE, BAND))
convicted, old_x0 = control_old_window()
print(" the pre-R-919 layout put the headline at x %d. The phone's crop edge is x %d, so %d px of it"
% (old_x0, round(PHONE_X0), round(PHONE_X0) - old_x0))
print(" were CUT on every phone; the measured safe edge is x %d, %d px further in (MARGIN %d)."
% (SAFE[0], SAFE[0] - old_x0, MARGIN))
check(convicted, "the safe check convicts the pre-R-919 layout (control)")
covers = [("cover-a.png", content_a(), (0.50, 0.78)), ("cover-b.png", content_b(), (0.68, 0.70)),
("cover-c.png", content_c(), (0.30, 0.74))]
cpaths = []
for name, content, glow in covers:
check_safe(name, content)
for name, (content, headline), glow in covers:
check_safe(name, content, headline)
cpaths.append(build_cover(name, content, glow))
except RuntimeError as e:
print("RENDER FAILED: %s" % e)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 97 KiB

File diff suppressed because one or more lines are too long