diff --git a/REUSE.md b/REUSE.md
index d49863df..dae4884a 100644
--- a/REUSE.md
+++ b/REUSE.md
@@ -173,6 +173,7 @@
| Rule-firing report | scripts/rules_report.py | Reads the `InstructionsLoaded` hook log and cross-references every `.claude/rules/*.md` in the workspace. **The column that matters is the empty one** — a rule that has never fired is mis-globbed or dead (built-but-never-wired, applied to instructions). Prints each silent rule's `paths:` so "wrong glob" is distinguishable from "quiet month". Caveat: the log only covers since the hook was armed. |
| Gate script | scripts/site_gates.py | Byte-level mechanical gates (BOM, emoji codepoint ranges, nav/footer diff, analytics, banned tokens, cache-bust); run `python scripts/site_gates.py` after ANY website change; non-zero exit on failure. |
| Hero clip builder | scripts/make_hero_clip.py | Assembles the home page's laptop clip from the operator's raw app recordings: scale+crop each to the screen's 1.626:1, crossfade, encode mp4 (h264) + webm (VP9) + a webp poster. **Runs on DooPlex** - Windows has no ffmpeg. NOT part of any build, gate or deploy: the site has no build step, so the output is committed like any other picture. Measured 2026-10-12: GIF is 11x the mp4 for the same seconds and dithers to 256 colours, hence mp4+webm. The frame it plays under is `website/assets/hero-laptop-frame.webp`, whose screen is cut out; the hole's position is in the CSS as percentages. **Every scene plays at its full recorded length** (operator ruling 2026-10-12: the raw file IS the edit); `--seconds N` only caps. One output set (`hero-clip.mp4/.webm`, `hero-poster.webp`) serves both languages; `--lang hu` adds a suffix back if they ever differ. |
+| Share-card builder | scripts/make_og_card.py | Rebuilds `og-image.png` / `og-image-en.png` (1200x630) around the laptop: the headline, logo and button are lifted as PIXELS from the previous card and composited with `lighter` onto flat `--bg-0`, because the site's font is a woff2 and nothing here can draw a glyph with it. **A new headline cannot be made with this script** - new words go back to the design tool, and this then takes that card as its source. Bump the `?v=` on the og:image URLs afterwards or no scraper will refetch. |
| Fetch-validate-install (shell) | scripts/felhom-host-install.sh `step_agent_install` (~L1108) | `fetch_raw` to mktemp → syntax-check (`bash -n`) → `install -m0755 -o root -g root` → only then activate; guarded-mkfs wrapper installed BEFORE the sudoers that references it (ordering is the safety property). All mutations through `run()` (dry-run aware). |
| Install-profile gate (shell) | scripts/felhom-host-install.sh `--mode appliance\|byo` (GL-2, v1.10.0) | Mandatory-flag profile (no default), refusals at argv time BEFORE any prompt/step, risky step gated at its CALL SITE (one auditable place — never a branch inside the step), mode persisted to state.json + resume-mismatch refusal, `FELHOM_INSTALL_STATE_DIR` override for harness isolation. Harness: scripts/hostinstall-mode-harness.sh (static refusal matrix + grep-invariants + PVE dry-transcript tier; red-proofs run against a mutated scratch copy). |
| Disclosure↔uninstall parity (shell) | scripts/felhom-host-install.sh `_uninstall_statement` + harness GL4-D (v1.11.0) | Every host artifact the byo disclosure names must be removed OR explicitly listed KEPT by `run_uninstall`; the harness greps the parity (token list). New install-time artifact ⇒ add its removal + disclosure line + parity token in the SAME commit. Drive data rule: plain `umount` only, never `-l`/`-f`, never any format op under /mnt/felhom-drives. |
diff --git a/scripts/make_og_card.py b/scripts/make_og_card.py
new file mode 100644
index 00000000..f0a6e78a
--- /dev/null
+++ b/scripts/make_og_card.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""make_og_card.py - rebuild the link-preview cards (og:image) around the laptop.
+
+ python scripts/make_og_card.py [--lang hu|en|both] [--check]
+
+WHAT IT IS FOR. The card Facebook, Messenger, LinkedIn and Slack show when somebody shares
+felhom.eu. The 2026-07-18 cards were a headline and two lines of text on a dark field: they showed
+the words and none of the product, and they predate the site's current look (operator, 2026-10-12:
+"the image is outdated"). This puts the same laptop the home page shows - the frame with the real
+screen recording's first frame in it - beside the headline.
+
+HOW IT KEEPS THE TYPOGRAPHY. The site's font is a woff2 (Plus Jakarta Sans); no font file on this
+machine or on DooPlex can be given to a drawing library, and installing one to draw two lines of text
+would be a new dependency for a once-a-year picture. So the headline, the logo and the button are
+taken as PIXELS from the previous card, which was rendered with the real font: they are cropped,
+the headline is scaled to free the right half, and they are composited onto a flat --bg-0 field with
+`lighter`, which makes the crop's own background disappear into the new one (both are #0A1220, so
+there is no seam to see). Nothing here draws a glyph.
+
+IF THE WORDS EVER CHANGE this script cannot help - a new headline needs the font, so the card goes
+back to the design tool, and this script then takes the new card as its source.
+
+INPUT website/assets/og-image.png, og-image-en.png (the previous cards - the typography)
+ website/assets/hero-laptop-frame.webp (the laptop, screen cut to transparency)
+ website/assets/hero-poster.webp (what the hero's clip opens on)
+OUTPUT the same two card files, 1200x630, overwritten.
+
+--check prints where the ink sits on the current cards and changes nothing.
+"""
+import argparse
+import os
+import sys
+
+from PIL import Image, ImageChops
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+A = os.path.join(ROOT, "website", "assets")
+
+W, H = 1200, 630
+BG = (10, 18, 32) # --bg-0, which is what sits behind the headline on the old cards
+TOPBAR_H = 6
+LEFT = 73 # the logo's own left edge on the old cards
+HEAD_TOP = 206 # where the headline's ink starts on the new card
+HEAD_WIDTH = 498 # how wide the headline is allowed to be, to leave the laptop its half
+LAPTOP_W = 548
+LAPTOP_X = 616
+# The screen hole in hero-laptop-frame.webp, measured off its alpha channel (2026-10-12):
+HOLE = (0.11041, 0.12345, 0.79817, 0.64305)
+
+CARDS = {
+ # lang: (file, headline band, pill band) - bands are y ranges on the OLD card, generous enough
+ # to hold accents and descenders
+ "hu": ("og-image.png", (200, 372), (510, 572)),
+ "en": ("og-image-en.png", (208, 382), (510, 572)),
+}
+LOGO_BAND = (76, 132)
+
+
+def ink_bbox(im, y0, y1, thresh=300):
+ """The bounding box of everything brighter than the background in a horizontal band."""
+ px = im.convert("RGB").load()
+ xs, ys = [], []
+ for y in range(y0, y1 + 1):
+ for x in range(im.size[0]):
+ if sum(px[x, y]) > thresh:
+ xs.append(x)
+ ys.append(y)
+ if not xs:
+ sys.exit("no ink in rows %d..%d - the source card is not the one this script expects" % (y0, y1))
+ return min(xs), min(ys), max(xs) + 1, max(ys) + 1
+
+
+def laptop():
+ """The frame with the hero's poster frame inside its cut-out screen."""
+ frame = Image.open(os.path.join(A, "hero-laptop-frame.webp")).convert("RGBA")
+ shot = Image.open(os.path.join(A, "hero-poster.webp")).convert("RGB")
+ fw, fh = frame.size
+ x, y = int(round(HOLE[0] * fw)), int(round(HOLE[1] * fh))
+ w, h = int(round(HOLE[2] * fw)), int(round(HOLE[3] * fh))
+ out = Image.new("RGBA", frame.size, (0, 0, 0, 0))
+ out.paste(shot.resize((w, h), Image.LANCZOS), (x, y))
+ out.alpha_composite(frame)
+ return out
+
+
+def build(lang, check):
+ src_name, head_band, pill_band = CARDS[lang]
+ src_path = os.path.join(A, src_name)
+ src = Image.open(src_path).convert("RGB")
+ if src.size != (W, H):
+ sys.exit("%s is %dx%d, expected %dx%d" % (src_name, src.size[0], src.size[1], W, H))
+
+ logo = ink_bbox(src, *LOGO_BAND)
+ head = ink_bbox(src, *head_band)
+ pill = ink_bbox(src, *pill_band)
+ if check:
+ print("%s logo %s headline %s button %s" % (src_name, logo, head, pill))
+ return
+
+ card = Image.new("RGB", (W, H), BG)
+ card.paste(src.crop((0, 0, W, TOPBAR_H)), (0, 0)) # the blue rule along the top
+
+ lap = laptop()
+ lh = int(round(LAPTOP_W * lap.size[1] / float(lap.size[0])))
+ card.paste(lap.resize((LAPTOP_W, lh), Image.LANCZOS), (LAPTOP_X, (H - lh) // 2),
+ lap.resize((LAPTOP_W, lh), Image.LANCZOS))
+
+ # the three text blocks, lifted as pixels and laid down with `lighter` so their own
+ # background (the same #0A1220) vanishes instead of leaving a rectangle
+ def lay(box, at, scale=1.0):
+ part = src.crop(box)
+ if scale != 1.0:
+ part = part.resize((max(1, int(round(part.size[0] * scale))),
+ max(1, int(round(part.size[1] * scale)))), Image.LANCZOS)
+ region = card.crop((at[0], at[1], at[0] + part.size[0], at[1] + part.size[1]))
+ card.paste(ImageChops.lighter(region, part), at)
+
+ lay(logo, (LEFT, logo[1]))
+ scale = HEAD_WIDTH / float(head[2] - head[0])
+ lay(head, (LEFT, HEAD_TOP), scale)
+ lay(pill, (LEFT - 1, pill[1]))
+
+ card.save(src_path, "PNG", optimize=True)
+ print("%-16s %4dx%d %6.1f KB (headline scaled %.2f, laptop %dpx)"
+ % (src_name, W, H, os.path.getsize(src_path) / 1024.0, scale, LAPTOP_W))
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--lang", default="both", choices=["hu", "en", "both"])
+ ap.add_argument("--check", action="store_true", help="print the measured bands, change nothing")
+ a = ap.parse_args()
+ for lang in (["hu", "en"] if a.lang == "both" else [a.lang]):
+ build(lang, a.check)
+ if not a.check:
+ print("Bump the ?v= on the og:image / twitter:image URLs, then re-scrape the page in "
+ "Facebook's Sharing Debugger - a card already attached to a post does not change.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md
index 9f604a90..b22f10bd 100644
--- a/website/CHANGELOG.md
+++ b/website/CHANGELOG.md
@@ -1,4 +1,26 @@
-## 2026-10-12 (review round) — the reviewer's findings, fixed
+## 2026-10-12 (night) — the share card shows the product
+
+The operator went to post the link and found the card out of date (2026-10-12): it was the
+2026-07-18 design — a headline and two lines of text on a dark field, nothing of the product, and
+older than everything the site has become since.
+
+- **`og-image.png` and `og-image-en.png` rebuilt around the laptop**, with the hero's own poster
+ frame on its screen: what somebody sees shared in a feed is now the thing itself.
+- **The typography is the old card's, to the pixel.** The site's font is a woff2 and no font file
+ here can be handed to a drawing library, so `scripts/make_og_card.py` lifts the headline, the logo
+ and the button out of the previous card as pixels and composites them with `lighter` onto flat
+ `--bg-0` — the crop's own background is the same colour, so there is no seam. **Nothing in that
+ script draws a glyph, so new words cannot be made with it**: a new headline goes back to the
+ design tool and the script then takes that card as its source. The two lines of lede are gone
+ (they would have needed the font); the laptop says what they said.
+- **`?v=2` on every `og:image` and `twitter:image`** (20 pages). A scraper caches by URL, so a new
+ picture at the old URL would have reached nobody. **A card already attached to a post does not
+ change** — that post has to drop the preview and re-add the link, after a re-scrape.
+- The English card carries the same Hungarian-language Immich screen as the Hungarian one, because
+ it is the same recording the hero uses on both pages. Worth a retake if the English page ever
+ gets its own.
+
+## 2026-10-12 (review round) — the reviewer's findings, fixed
A page-by-page review of the live site (Playwright, 1440 px and 390 px) found six real defects and
two editorial questions. The operator answered three questions first; the answers are in
diff --git a/website/adatkezeles.html b/website/adatkezeles.html
index be8f47d2..a444321b 100644
--- a/website/adatkezeles.html
+++ b/website/adatkezeles.html
@@ -15,7 +15,7 @@
-
+
diff --git a/website/alkalmazasok.html b/website/alkalmazasok.html
index 6c430ddc..ca09ccd4 100644
--- a/website/alkalmazasok.html
+++ b/website/alkalmazasok.html
@@ -18,14 +18,14 @@
-
+
-
+