diff --git a/REPORT-facebook-cover-r919.md b/REPORT-facebook-cover-r919.md new file mode 100644 index 00000000..e65f5bce --- /dev/null +++ b/REPORT-facebook-cover-r919.md @@ -0,0 +1,147 @@ +# REPORT — R-919: the Facebook cover rebuilt to the measured phone view (2026-10-09) + +A `REPORT-.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. diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 21c9d211..da215f1b 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -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) diff --git a/marketing/CHANGELOG.md b/marketing/CHANGELOG.md index 5a26b2af..fe4be2c4 100644 --- a/marketing/CHANGELOG.md +++ b/marketing/CHANGELOG.md @@ -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. diff --git a/marketing/facebook/README.md b/marketing/facebook/README.md index 460b898c..201839ca 100644 --- a/marketing/facebook/README.md +++ b/marketing/facebook/README.md @@ -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 diff --git a/marketing/facebook/build.py b/marketing/facebook/build.py index 34ad5d6c..4a63de7c 100644 --- a/marketing/facebook/build.py +++ b/marketing/facebook/build.py @@ -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">' % (src, safe, b64(rec), pct(dk[0], CW), pct(dk[1], CH), pct(dk[2], CW))) crop_l = PHONE_X0 / CW_PHONE * 100 - phone = ('
' + phone = ('
' + '' '
' - % (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('

%s

Computer (820 x 312), dashed = safe area (preview only)

%s' - '

Phone (640 x 360 crop)

%s
' % (label, desk, phone)) + '

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.

%s' + % (label, desk, round(CW_PHONE), CW, round(ph[2]), + round(ph[2] * PHONE_HDR[0] / CW_PHONE), round(PHONE_HDR[0]), phone)) html = """ Facebook Page pictures @@ -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%%}

Felhom.eu - Facebook pictures

+

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.

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.

Profile picture

%s %s
-""" % (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) diff --git a/marketing/facebook/out/cover-a.png b/marketing/facebook/out/cover-a.png index a0720446..1abbb1e8 100644 Binary files a/marketing/facebook/out/cover-a.png and b/marketing/facebook/out/cover-a.png differ diff --git a/marketing/facebook/out/cover-b.png b/marketing/facebook/out/cover-b.png index 5c9531af..06b47ceb 100644 Binary files a/marketing/facebook/out/cover-b.png and b/marketing/facebook/out/cover-b.png differ diff --git a/marketing/facebook/out/cover-c.png b/marketing/facebook/out/cover-c.png index 6e6fe73b..6426cdb2 100644 Binary files a/marketing/facebook/out/cover-c.png and b/marketing/facebook/out/cover-c.png differ diff --git a/marketing/facebook/out/preview.html b/marketing/facebook/out/preview.html index edd5181e..486c8444 100644 --- a/marketing/facebook/out/preview.html +++ b/marketing/facebook/out/preview.html @@ -11,12 +11,15 @@ 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%}

Felhom.eu - Facebook pictures

+

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 938 px of the 1640-pixel width, with a profile +circle over the bottom middle 393 cover-pixels across. It is NOT measured in the Facebook app — check there too.

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.

Profile picture

Dark background (recommended) — 176 px, 96 px, 40 px
White background — 176 px, 96 px, 40 px
-

A - calm

Computer (820 x 312), dashed = safe area (preview only)

Phone (640 x 360 crop)

B - the idea

Computer (820 x 312), dashed = safe area (preview only)

Phone (640 x 360 crop)

C - the product

Computer (820 x 312), dashed = safe area (preview only)

Phone (640 x 360 crop)

+

A - calm

Computer (820 x 312), dashed = safe area (preview only)

Phone — MEASURED (R-919): the centre 938 px of 1640 at full height, and the profile circle 393 cover-pixels across - 173 px on a 412 px phone screen - centred. NOT 640 x 360, and NOT Meta’s 2,4:1.

B - the idea

Computer (820 x 312), dashed = safe area (preview only)

Phone — MEASURED (R-919): the centre 938 px of 1640 at full height, and the profile circle 393 cover-pixels across - 173 px on a 412 px phone screen - centred. NOT 640 x 360, and NOT Meta’s 2,4:1.

C - the product

Computer (820 x 312), dashed = safe area (preview only)

Phone — MEASURED (R-919): the centre 938 px of 1640 at full height, and the profile circle 393 cover-pixels across - 173 px on a 412 px phone screen - centred. NOT 640 x 360, and NOT Meta’s 2,4:1.