diff --git a/docs/main.js b/docs/main.js
index 443da0d..c8c51b1 100644
--- a/docs/main.js
+++ b/docs/main.js
@@ -7,6 +7,8 @@
fr: "Ton notch répond à la question. La progression de ton cours, là où tu regardes déjà." },
"cta.download":{ en: "Download for Mac", fr: "Télécharger pour Mac" },
"meta.reqs": { en: "macOS 14+ · Apple Silicon · free & open source", fr: "macOS 14+ · Apple Silicon · gratuit & open source" },
+ "hero.shot": { en: "NotchBar expanded in the notch, showing the current event's progress",
+ fr: "NotchBar déployé dans le notch, montrant la progression de l'événement en cours" },
"mock.title": { en: "Organic Chemistry", fr: "Chimie organique" },
"mock.left": { en: "23 min left", fr: "23 min restantes" },
"feat.heading":{ en: "What it does", fr: "Ce que ça fait" },
@@ -90,5 +92,13 @@
b.addEventListener("click", function () { apply(b.getAttribute("data-lang")); });
});
+ // Swap the CSS mock for the real screenshot, but only once it has loaded:
+ // a missing asset then costs nothing instead of showing a broken image.
+ var shot = document.querySelector(".hero-shot");
+ if (shot) {
+ shot.addEventListener("load", function () { shot.hidden = false; });
+ if (shot.complete && shot.naturalWidth > 0) shot.hidden = false;
+ }
+
apply(resolveLang());
})();
diff --git a/docs/style.css b/docs/style.css
index c30486a..6ec4e16 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -30,6 +30,13 @@ header.site,footer.site{max-width:var(--max);margin:0 auto;padding:22px 24px;
.hero h1{font-size:clamp(44px,8vw,92px);font-weight:850;letter-spacing:-.045em;line-height:.9;margin:0 0 20px}
.hero .sub{color:var(--muted);font-size:17px;max-width:42ch;margin:0 0 24px}
.hero-stage{display:flex;align-items:center}
+/* The real screenshot, revealed by main.js only once it actually loads. Until
+ docs/assets/hero.png exists — or with JS off — it stays hidden and the CSS
+ mock below is what the page shows. No broken image, ever. */
+.hero-shot{width:100%;height:auto;border-radius:14px;
+ box-shadow:0 30px 60px -34px rgba(0,0,0,.6)}
+.hero-shot:not([hidden]){display:block}
+.hero-shot:not([hidden]) + .desktop{display:none}
.desktop{position:relative;width:100%;border-radius:14px 14px 10px 10px;overflow:hidden;
background:radial-gradient(120% 90% at 20% -10%,#1a2340 0,rgba(26,35,64,0) 55%),
diff --git a/scripts/capture-hero.sh b/scripts/capture-hero.sh
new file mode 100755
index 0000000..d4fe0fe
--- /dev/null
+++ b/scripts/capture-hero.sh
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+# Captures the hero screenshot used on the landing page (docs/assets/hero.png).
+#
+# Usage:
+# capture-hero.sh capture directly, after a countdown
+# capture-hero.sh --from crop an existing full-screen screenshot
+# capture-hero.sh --delay 8 seconds before the direct capture fires
+#
+# NotchBar must be running: the crop region is read from the panel's real window
+# rather than hardcoded, so it cannot drift from the app's geometry.
+#
+# The direct mode needs Screen Recording permission for your terminal (System
+# Settings > Privacy & Security > Screen Recording). Without it, screencapture
+# fails with "could not create image from rect" — use --from instead: take a
+# full-screen shot with Cmd-Shift-5 (Options > Timer, so the panel stays open),
+# then pass the file. That path needs no permission at all.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+OUT="$ROOT/docs/assets/hero.png"
+DELAY=6
+FROM=""
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --from) FROM="${2:-}"; shift 2 ;;
+ --delay) DELAY="${2:-}"; shift 2 ;;
+ *) echo "error: unknown argument '$1'" >&2; exit 1 ;;
+ esac
+done
+
+# Margin around the panel, in points: enough menu bar on either side for the
+# notch to read as a notch, and a little desktop underneath.
+SIDE_MARGIN=150
+BOTTOM_MARGIN=40
+
+read -r X Y W H SCREEN_W SCALE < ($1["Width"] ?? 0) }
+
+guard let panel = panels.first, let pw = panel["Width"], let ph = panel["Height"] else {
+ FileHandle.standardError.write(Data("error: NotchBar is not running\n".utf8))
+ exit(1)
+}
+
+// Window bounds are already top-left origin, and the panel bleeds 2pt above the
+// screen, so clamp the top edge to 0.
+let x = max((panel["X"] ?? 0) - sideMargin, 0)
+let y = max(panel["Y"] ?? 0, 0)
+let width = min(pw + sideMargin * 2, screen.frame.width - x)
+let height = ph + bottomMargin
+
+print("\(Int(x)) \(Int(y)) \(Int(width)) \(Int(height)) \(Int(screen.frame.width)) \(Int(screen.backingScaleFactor))")
+SWIFT
+)
+EOF
+
+echo "==> Region: ${W}x${H} pt at (${X},${Y}) — screen ${SCREEN_W}pt @${SCALE}x"
+mkdir -p "$(dirname "$OUT")"
+
+if [ -n "$FROM" ]; then
+ [ -f "$FROM" ] || { echo "error: no such file: $FROM" >&2; exit 1; }
+
+ SHOT_W=$(sips -g pixelWidth "$FROM" | awk '/pixelWidth/{print $2}')
+ # A full-screen shot spans the screen, so its own width gives the true
+ # pixel-per-point ratio — more reliable than assuming the backing scale.
+ RATIO=$(awk -v a="$SHOT_W" -v b="$SCREEN_W" 'BEGIN{printf "%.6f", a/b}')
+ echo "==> Cropping $FROM (${SHOT_W}px wide, ${RATIO} px/pt)"
+
+ px() { awk -v v="$1" -v r="$RATIO" 'BEGIN{printf "%d", v*r+0.5}'; }
+ sips -c "$(px "$H")" "$(px "$W")" --cropOffset "$(px "$Y")" "$(px "$X")" \
+ "$FROM" --out "$OUT" >/dev/null
+else
+ echo "==> Capturing in ${DELAY}s — hover the notch now and keep the cursor on it"
+ screencapture -T "$DELAY" -x -R "$X,$Y,$W,$H" "$OUT" || {
+ echo "error: screencapture failed. Grant Screen Recording to your terminal," >&2
+ echo " or take a full-screen shot with Cmd-Shift-5 and pass --from." >&2
+ exit 1
+ }
+fi
+
+OUT_W=$(sips -g pixelWidth "$OUT" | awk '/pixelWidth/{print $2}')
+OUT_H=$(sips -g pixelHeight "$OUT" | awk '/pixelHeight/{print $2}')
+echo "==> Done: $OUT (${OUT_W}x${OUT_H}px)"
+[ "$OUT_W" -lt 1000 ] && echo "warning: under 1000px wide — it will look soft on a retina display" >&2
+echo " Commit it, and the landing page picks it up automatically."
+exit 0