diff --git a/REPORT-facebook-cover-r919.md b/REPORT-facebook-cover-r919.md
index ba7d6519..9fa64096 100644
--- a/REPORT-facebook-cover-r919.md
+++ b/REPORT-facebook-cover-r919.md
@@ -214,6 +214,52 @@ under the profile circle on all three covers, and the renders were looked at at
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.
+## 12. Second measurement: a phone has TWO views (2026-10-09)
+
+The operator checked the Page signed out on a phone and found the cover **not** cropped at the sides — the
+opposite of what §4 says — and then checked it signed in on his real phone, where it **is** cropped. Both
+observations are right. My R-919 measurement was not wrong; it was **one view, generalised to "the phone"**.
+
+**Signed in, on the operator's real phone (Chrome on Android, 1080 px).** Measured from the screenshot, not
+by eye: the cover band is y 401–1111 = 708 px tall across the full 1080 px screen = **1.525 : 1** against the
+file's 2.628 : 1, so the width is cropped at full height. Solving the laptop frame's left edge (cover x 814,
+screen x 528) against the scale gives a visible window of **x 351 .. 1298**; R-919's emulator said
+**351 .. 1289**. The left edge agrees to the pixel, and the predicted screen position of the text's left edge
+(x 75) matches the picture. **R-919 is confirmed on real hardware.** Its circle: x 649–1028 from y 450,
+against the emulator's 637–1030 from y 369.
+
+**Signed out.** Also measured from the picture: the cover box is 412 × 132 = **3.121 : 1**, wider than the
+file, so here the **height** is cut, not the width — and the file's blue top rule is still visible at the top
+edge (rgb 0,121,199 at the first two rows), which puts the crop at the **bottom**: the top **525 px** of 624
+survives, the bottom 99 px does not. The profile circle is far bigger and higher: **x 486–1150 from y 232**,
+41 % of the width.
+
+**What that means for a cover.** The sides are cut for one visitor and the bottom for the other, so the two
+constraints have to be met together: SAFE is now **(391, 40)–(1249, 485)** and the binding circle is the
+signed-out one. Two changes made the result workable rather than merely safe:
+
+- **The circles are DISCS.** They were modelled as rectangles running to the bottom of the cover. Near its
+ top a disc is only a few pixels wide, so the rectangle was discarding most of the lower cover for
+ nothing — a large part of why the frame looked empty.
+- **Two layers.** The READ layer (catchphrase, wordmark) must survive every view; the check fails on it.
+ The DECOR layer (the laptop, the home motif) may be cropped or covered, and the build **reports** the
+ cost instead of forbidding it — cover B loses 39 % of its decor in some view, cover C 62 %. Before the
+ split, one check governed both, which is why no laptop big enough to read could ever pass.
+
+**Red-proof, again.** Before redrawing anything, the two-view geometry was run against the covers as they
+then stood: **all three convicted** — cover A 908 content pixels under a circle (its wordmark sat inside the
+signed-out circle), cover B 1715 plus content past the cut bottom, cover C 1962. The permanent
+`control_old_window` control still convicts the original pre-R-919 layout too, so the build now carries two
+controls.
+
+**The redraw.** Cover C is the operator's own idea: catchphrase and wordmark on the left, the dashboard on
+the right at **640 px wide** (was 370) running off the right edge — readable for the first time. Cover B's
+home motif grew the same way. Cover A is unchanged in kind, lifted into the tighter band. All three keep
+every capital above the 25 px floor (48, 45, 43 px).
+
+**Still unmeasured: the Facebook app.** Three views are now measured and they disagree with each other and
+with Meta's help page. The app is the fourth and the most used. R-919 stays **VERIFY** for that reason.
+
## 10. Teardown
Nothing on any host, the hub or Facebook. The work ran in the two git working trees only; the throwaway
diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md
index 3395268c..a4e20c51 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. **-- 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 |
+| **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. **-- 2026-10-09 (second measurement):** a phone has TWO views and they disagree. SIGNED IN the sides are cut (confirmed on the operator's REAL phone, Chrome/Android: the window solves to x 351..1298 against the emulator's 351..1289 - the left edge to the pixel). SIGNED OUT the FULL width is shown but the cover is top-anchored and only the top 525 px survives (the bottom 99 px is cut; the file's blue top rule is still visible, which is how the side was established), with a much bigger, higher circle at x 486..1150 from y 232. `build.py` now carries both views, models the circles as DISCS rather than rectangles running to the bottom, and splits the artwork into a READ layer (must survive every view) and a DECOR layer (may be cropped or covered; the build reports the cost - B 39 %, C 62 %). The two-view geometry convicted all three then-current covers before the redraw (A 908 px under a circle, B 1715, C 1962). Covers redrawn: C is the operator's laptop idea with the dashboard at 640 px (was 370), B's motif grown to match. | **VERIFY -- rebuilt on `main` 2026-10-09; three views measured, the Facebook APP still is not.** The three measured views disagree with each other and with Meta's help page, so the app is the one that decides. | — | Operator: upload the rebuilt cover (and the profile picture) by hand, 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 crops differently again, send a screenshot - the last two were solved straight out of the picture - and move `PHONE_IN_X` / `PHONE_OUT_Y` / `PHONE_CIRCLES`; the designs follow them | CC |
## Process & tooling — 23 rows (P3 3, P4 20)
diff --git a/marketing/CHANGELOG.md b/marketing/CHANGELOG.md
index 9838e16e..90f9511b 100644
--- a/marketing/CHANGELOG.md
+++ b/marketing/CHANGELOG.md
@@ -1,5 +1,38 @@
# marketing — CHANGELOG
+## facebook — a phone has TWO views of the cover, not one; the covers fill the frame again (2026-10-09)
+
+The operator saw the cover UNCROPPED on a signed-out phone and asked why, after R-919 had said the sides
+are cut. Both are true, and the first measurement was one view generalised.
+
+- **Measured, signed out** (from the operator's screenshot, solved against the picture): Facebook shows
+ the **full width** — nothing is cut at the sides — but the cover is **top-anchored and only the top
+ 525 px survives**; the bottom 99 px is cut. The blue rule from the top of the file is still visible at
+ the top edge, which is how the crop's side was established. The profile circle is much bigger and much
+ higher than in the signed-in view: **x 486–1150 from y 232**.
+- **Measured, signed in, on the operator's REAL phone** (Chrome on Android, 1080 px): the crop is real.
+ Solving the screenshot against the laptop frame put the window at **x 351..1298** where the emulated
+ Pixel 9 had said **351..1289** — the left edge to the pixel. R-919's number is confirmed, not replaced.
+- **So the sides are cut for a signed-in visitor and the bottom for a signed-out one**, and a cover has to
+ survive both. `build.py` now carries both views (`PHONE_IN_X`, `PHONE_OUT_Y`, `PHONE_CIRCLES`) instead of
+ one, and SAFE is their intersection: **(391, 40)–(1249, 485)**.
+- **The profile circles are modelled as DISCS now, not rectangles reaching the bottom.** Near its top a
+ disc is only a few pixels wide; the rectangle threw away most of the cover for nothing and is part of
+ why the covers looked empty.
+- **Two layers, because a cover has two kinds of pixel.** The READ layer (catchphrase + wordmark) must
+ survive every view and the check fails on it. The DECOR layer (laptop, home motif) may be cropped or
+ covered — and the build now REPORTS how much is lost rather than forbidding it: cover B 39 %, cover C
+ 62 %. Without that split the check forbade a laptop big enough to see, which is what left the frame bare.
+- **The covers were redrawn.** C is the operator's laptop idea: catchphrase and wordmark left, the
+ dashboard now **640 px wide** (was 370) and running off the right edge — legible at last. B's home motif
+ grew the same way. A is unchanged in kind, lifted into the tighter band.
+- **A second red-proof, run before the redraw:** the two-view geometry convicted all three covers as they
+ stood — cover A 908 content pixels under a circle, B 1715 plus content past the bottom, C 1962. The
+ permanent `control_old_window` still convicts the pre-R-919 layout as well.
+- Profile pictures untouched; they are not in the diff. Still **R-919 = VERIFY**: the Facebook **app** is
+ the one view nobody has measured.
+
+
## 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
diff --git a/marketing/facebook/build.py b/marketing/facebook/build.py
index 3e62fbab..23adc8f2 100644
--- a/marketing/facebook/build.py
+++ b/marketing/facebook/build.py
@@ -85,36 +85,47 @@ LOGO_R = 0.40 # where the logo's farthest pixel is placed (inside the che
CW, CH = 1640, 624 # cover, 2 x the 820 x 312 computer display
SCALE = CW / 820.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
+# --- the phone views: MEASURED, and there are TWO of them (R-919) ------------------------------------
+# A phone does NOT show the cover one way. Both of these are measured on the live Page:
+#
+# SIGNED IN 2026-10-08 in Chrome DevTools device mode (Pixel 9, mobile UA, after a reload - without
+# one Facebook keeps serving the desktop bundle), and CONFIRMED 2026-10-09 on the
+# operator's own phone (Chrome on Android, 1080 px). Facebook keeps the cover's FULL
+# HEIGHT and shows only the centre ~948 px of its width. Solving the real-phone shot
+# against the laptop frame put the window at x 351..1298 where the emulator said
+# 351..1289 - the left edge to the pixel. Profile circle: x 637..1030 from y 369
+# (emulated), x 649..1028 from y 450 (real phone).
+# SIGNED OUT 2026-10-09, measured from the operator's screenshot. The FULL width is shown - nothing
+# is cut at the sides - but the cover is top-anchored and only the TOP 525 px survives;
+# the bottom 99 px is cut. The circle is much bigger and much higher: x 486..1150 from
+# y 232.
+#
+# So the SIDES are cut for a signed-in visitor and the BOTTOM for a signed-out one, and a cover has to
+# survive both. Meta's own help page describes neither. Evidence and the recipe to re-measure:
+# documentation/audits/facebook-page-setup-2026-10-08/.
+# STILL NOT MEASURED: the Facebook app. That is why MARGIN is deliberate and why the operator's check
+# in the app remains the acceptance step rather than this build.
+PHONE_IN_X = (351.0, 1289.0) # signed in: the visible width, at full height
+PHONE_OUT_Y = 525.0 # signed out: the visible height, at full width
MARGIN = 40
-SAFE = (int(math.ceil(PHONE_X0 + MARGIN)), MARGIN, int(CW - PHONE_X0 - MARGIN), CH - MARGIN)
+SAFE = (int(math.ceil(PHONE_IN_X[0] + MARGIN)), MARGIN,
+ int(PHONE_IN_X[1] - MARGIN), int(PHONE_OUT_Y - MARGIN))
PAD = 24
# 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
+# 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.
+# The two phone circles, as MEASURED. They are discs, and modelling them as rectangles that reach the
+# bottom threw away most of the cover for nothing: near its top a disc is only a few pixels wide.
+PHONE_CIRCLES = [(637.0, 369.0, 393.0), # signed in
+ (486.0, 232.0, 669.0)] # signed out - bigger and higher, so it is the binding one
+DISCS = [(c[0] + c[2] / 2.0, c[1] + c[2] / 2.0, c[2] / 2.0 + PAD) for c in PHONE_CIRCLES]
+QUIET = [(0, DESK_CIRCLE[1] - PAD, DESK_CIRCLE[0] + DESK_CIRCLE[2] + PAD, CH)]
+# The band a CENTRED block may use: inside SAFE, clear of the computer circle, above the higher of the
+# two phone circles. A block that stays to one side may go lower - the disc test is the real arbiter.
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)
+ SAFE[2], int(min(d[1] - d[2] for d in DISCS)))
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.
@@ -321,15 +332,15 @@ def background(glow_at=(0.78, 0.25)):
def content_a():
- """Calm: the one line, centred in the band, with the domain under it."""
+ """Calm: the one line, centred in the band, with the wordmark under it. No decor by design."""
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
mid = (BAND[0] + BAND[2]) / 2.0
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
+ x = headline(c, x, 118, L1 + " ", size, TEXT1)
+ headline(c, x, 118, L2, size, BLUE_BRIGHT)
+ wordmark(c, mid, 134, round(size * 0.95), align="center")
+ return c, None, size
def rack(d, x, y, w, h, fill, dot):
@@ -375,23 +386,25 @@ def content_b():
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = BAND[0] + 12
- 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"),
- (928, 262, "folder"), (1155, 425, "photo"), (1110, 540, "doc")]
+ size = headline_fit(L2, 430, 58) # the READ column; the motif gets the rest
+ headline(c, tx, 100, L1, size, TEXT1)
+ headline(c, tx, 100 + size + 12, L2, size, BLUE_BRIGHT)
+ wordmark(c, tx, 100 + size + 26, round(size * 0.72))
+ # DECOR: the home with its server and its apps, free to run past the crop and under the circles
+ dec = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
+ d = ImageDraw.Draw(dec)
+ hx, hy, hw = 1080, 210, 230 # house body: left, top, width
+ tiles = [(960, 96, "photo"), (1150, 70, "doc"), (1400, 150, "play"), (1430, 360, "lock"),
+ (905, 300, "folder"), (1230, 520, "photo"), (980, 505, "doc")]
for x, y, _ in tiles:
- 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)
+ d.line((hx + hw / 2, hy + 110, x, y), fill=(46, 168, 245, 110), width=5)
+ d.rectangle((hx, hy, hx + hw, hy + 200), fill=BG1, outline=BLUE_BRIGHT, width=7)
+ d.polygon([(hx - 46, hy + 5), (hx + hw / 2, hy - 130), (hx + hw + 46, hy + 5)], fill=BG1, outline=BLUE_BRIGHT)
+ d.line([(hx - 46, hy + 5), (hx + hw / 2, hy - 130), (hx + hw + 46, hy + 5)], fill=BLUE_BRIGHT, width=7, joint="curve")
+ rack(d, hx + 42, hy + 42, hw - 84, 116, BLUE, TEXT1)
for x, y, kind in tiles:
- tile(d, x, y, 62, kind)
- return c, size
+ tile(d, x, y, 92, kind)
+ return c, dec, size
def content_c():
@@ -400,23 +413,26 @@ def content_c():
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = BAND[0] + 12
- 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))
+ size = headline_fit(L2, 400, 58)
+ headline(c, tx, 104, L1, size, TEXT1)
+ headline(c, tx, 104 + size + 12, L2, size, BLUE_BRIGHT)
+ wordmark(c, tx, 104 + size + 26, round(size * 0.80))
+ # DECOR: the dashboard, now big enough to read, running off the right edge and under the circles
+ dec = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
+ d = ImageDraw.Draw(dec)
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 = 370
+ sw = 640
sh = int(round(sw * shot.height / shot.width))
shot = shot.resize((sw, sh), Image.LANCZOS)
- fx, fy, b = 814, 56, 14 # frame left/top, bezel
+ fx, fy, b = 880, 46, 24 # 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))
+ dec.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),
+ d.polygon([(fx - 52, base_y + 38), (fx + sw + 2 * b + 52, base_y + 38), (fx + sw + 2 * b, base_y),
(fx, base_y)], fill=LINE)
- return c, size
+ return c, dec, size
def cap_height(size):
@@ -435,6 +451,27 @@ def control_old_window():
return x0 < SAFE[0], x0
+def decor_cost(dec):
+ """How much of the decor layer a visitor never sees: cut by the signed-in crop, cut by the
+ signed-out bottom, or behind a profile circle. Reported, never failed - decor is allowed to go
+ there, and that is the whole point of having a second layer."""
+ if dec is None:
+ return 0, 0
+ pts = alpha_points(dec.resize((CW // 4, CH // 4)), thr=8)
+ if not pts:
+ return 0, 0
+ tot = len(pts)
+ lost = 0
+ for q in pts:
+ x, y = q[0] * 4, q[1] * 4
+ if x < PHONE_IN_X[0] or x > PHONE_IN_X[1] or y > PHONE_OUT_Y:
+ lost += 1
+ continue
+ if any((x - cx) ** 2 + (y - cy) ** 2 <= r * r for cx, cy, r in DISCS):
+ lost += 1
+ return tot, lost
+
+
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]
@@ -442,18 +479,22 @@ def check_safe(name, content, headline):
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 = []
- for q in QUIET:
- 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]))
+ q = QUIET[0]
+ desk = sum(1 for x, y in zip(xs, ys) if q[0] <= x <= q[2] and q[1] <= y <= q[3])
+ phone = 0
+ for cx, cy, r in DISCS:
+ phone += sum(1 for x, y in zip(xs, ys) if (x - cx) ** 2 + (y - cy) ** 2 <= r * r)
+ check(desk + phone == 0, "%s: %d content pixels under the computer circle, %d under a phone circle"
+ % (name, desk, phone))
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):
+def build_cover(name, content, dec, glow):
img = background(glow)
+ if dec is not None:
+ img.alpha_composite(dec)
img.alpha_composite(content)
path = os.path.join(OUT, name)
img.convert("RGB").save(path, optimize=True)
@@ -468,15 +509,25 @@ def b64(path):
def preview(profiles, covers):
- dk, ph = DESK_CIRCLE, PHONE_CIRCLE
+ dk = DESK_CIRCLE
sx0, sy0, sx1, sy1 = SAFE
pct = lambda v, tot: "%.3f%%" % (100.0 * v / tot) # noqa: E731
prof_html = "".join(
'
'
- '
%s — 176 px, 96 px, 40 px (the mark only)'
+ '%s \u2014 176 px, 96 px, 40 px (the mark only)'
% (b64(p), b64(p), b64(p), label) for label, p in profiles)
cov = []
rec = profiles[0][1]
+
+ def phone_panel(src, vx0, vx1, vy1, circ):
+ """One phone view: the window (vx0..vx1 of the width, 0..vy1 of the height) with its circle."""
+ vw, vh = vx1 - vx0, vy1
+ return ('
Computer (820 x 312), dashed = safe area (preview only)
%s'
- '
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.
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.
+
A phone does NOT show the cover one way, so each cover is shown twice below. Both are MEASURED
+on the live Page (R-919). Signed in: the centre %d px of the %d-pixel width, at full height - confirmed
+on a real phone, where the window solved to x 351..1298 against the emulator's 351..1289. Signed out:
+the full width, but top-anchored and only the top %d px - the bottom %d px is cut - with a bigger, higher
+circle. So the sides are cut for one visitor and the bottom for the other. The Facebook APP is still not
+measured - please check there.
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
@@ -524,7 +579,7 @@ covers below show the dark one.
Profile picture
%s
%s
-""" % (round(CW_PHONE), CW, round(PHONE_CIRCLE[2]),
+""" % (round(PHONE_IN_X[1] - PHONE_IN_X[0]), CW, round(PHONE_OUT_Y), CH - round(PHONE_OUT_Y),
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:
@@ -550,21 +605,26 @@ 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 %d x %d; the phone shows the centre %d px of %d - MEASURED, R-919)"
- % (CW, CH, round(CW_PHONE), CW))
+ print("safe-area check (computer %d x %d; signed in a phone shows x %d..%d at full height, signed"
+ % (CW, CH, round(PHONE_IN_X[0]), round(PHONE_IN_X[1])))
+ print(" out the full width but only the top %d px - all MEASURED, R-919)" % round(PHONE_OUT_Y))
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))
+ % (old_x0, round(PHONE_IN_X[0]), round(PHONE_IN_X[0]) - 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, headline), glow in covers:
+ for name, (content, dec, headline), glow in covers:
check_safe(name, content, headline)
- cpaths.append(build_cover(name, content, glow))
+ tot, lost = decor_cost(dec)
+ if tot:
+ print(" %s: decor %d px, %d (%d %%) cropped or behind a circle in some view - allowed"
+ % (name, tot, lost, round(100.0 * lost / tot)))
+ cpaths.append(build_cover(name, content, dec, glow))
except RuntimeError as e:
print("RENDER FAILED: %s" % e)
return 2
diff --git a/marketing/facebook/out/cover-a.png b/marketing/facebook/out/cover-a.png
index a76d1825..c28f0cec 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 6ed6ab2d..2280a7ae 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 02de4205..5327629e 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 9cf18e99..d5e9b2e7 100644
--- a/marketing/facebook/out/preview.html
+++ b/marketing/facebook/out/preview.html
@@ -4,25 +4,30 @@
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.
+
A phone does NOT show the cover one way, so each cover is shown twice below. Both are MEASURED
+on the live Page (R-919). Signed in: the centre 938 px of the 1640-pixel width, at full height - confirmed
+on a real phone, where the window solved to x 351..1298 against the emulator's 351..1289. Signed out:
+the full width, but top-anchored and only the top 525 px - the bottom 99 px is cut - with a bigger, higher
+circle. So the sides are cut for one visitor and the bottom for the other. The Facebook APP is still not
+measured - please check there.
Profile picture: the logo MARK only — no „felhom.eu” 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 76 % 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.
Profile picture
Dark background (recommended) — 176 px, 96 px, 40 px (the mark only)
White background — 176 px, 96 px, 40 px (the mark only)
-
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.
+
A - calm
Computer — the whole picture, and the profile circle sits BELOW it. Dashed = the safe area (preview only).
Phone, signed in — the centre 938 px of 1640, full height
Phone, signed out — the full width, but only the top 525 px
B - the idea
Computer — the whole picture, and the profile circle sits BELOW it. Dashed = the safe area (preview only).
Phone, signed in — the centre 938 px of 1640, full height
Phone, signed out — the full width, but only the top 525 px
C - the product
Computer — the whole picture, and the profile circle sits BELOW it. Dashed = the safe area (preview only).
Phone, signed in — the centre 938 px of 1640, full height
Phone, signed out — the full width, but only the top 525 px