docs(agent): phase 04 - reachable share CTA at narrow widths

The Share button on /mirror is unclickable below ~700px and below the
fold in short windows. Measured with elementFromPoint on Chromium:

  320x568  stage 288x180  button 187px past the stage bottom  hit=H2
  390x844  stage 358x224  button 113px past                   hit=LABEL
  480x800  stage 448x280  button  36px past                   hit=#share-link
  900x480  stage 868x543  inside the stage, below the viewport hit=null

Root cause: .stage is display:grid with place-items:center, so <video>,
.placeholder and .livetag occupy three stacked rows rather than one
overlay cell. An idle <video> is intrinsically 300x150, so the
placeholder always starts at stage.top + 151 and runs out of the
overflow:hidden box; aspect-ratio 16/10 with no min/max height then
clips it when narrow and overshoots the viewport when short.

#stop measures clickable everywhere (the placeholder is hidden when
live), so the fix is scoped to the idle stage.

Phase: one-cell stage, floor + ceiling + scroll escape, a 12-viewport
clickability sweep (idle and live), and stylesheet guards.

LLM-Generated: true
This commit is contained in:
2026-11-01 19:55:54 -05:00
parent 7d4bcf7e23
commit f3de5b70ff
5 changed files with 467 additions and 0 deletions
@@ -0,0 +1,133 @@
# Phase 04 — The Share button must always be clickable
**Source:** user request in chat — "the share button is cut off by the preview-stage, I can't click it in narrower view widths."
**Story:** n/a
**Context:** `src/mirror/static/app.css` `.share` / `.stage` / `.placeholder` / `.btn.big` (the `/* share */` block, ~L220–250), `src/mirror/static/mirror.html` (`#preview-stage` > `#preview`, `#placeholder` > `#start`, `#livetag`), `tests/test_app.py::TestStylesheet`, `tests/e2e/`
## Objective
`#start` ("Share my screen") is reachable and clickable at every viewport a
person might realistically open `/mirror` on — measured from 320×568 to 1920×1080,
and on short windows like 900×480. Today it is not, and this is not a cosmetic
wrinkle: a sharer on a phone, a narrow side-by-side window, or a small laptop
window **cannot start sharing at all**.
## Measured on the reference host (Chromium, `channel="chromium"`, `http://127.0.0.1`)
| Viewport | `#preview-stage` box | `#start` vs the stage box | `document.elementFromPoint(colour of button centre)` |
|---|---|---|---|
| 320×568 | 288×180 | button bottom **187 px below** the stage | `H2` — not the button |
| 360×640 | 328×205 | 132 px below | the panel's `#name` input |
| 390×844 | 358×224 | 113 px below | a `LABEL` |
| 480×800 | 448×280 | 36 px below | `#share-link` |
| 700×900 | 668×418 | inside | `#start` ✅ |
| 900×480 | 868×543 | inside the stage, **below the viewport** | `null` |
| 1280×450 | 902×564 | inside, **below the viewport** | `null` |
Two independent failure modes, and the diagnosis is exact:
1. **The stage is a two-row grid, not an overlay.** `.stage` is
`display: grid; place-items: center`, and its children — `<video id="preview">`,
`.placeholder`, `.livetag` — are three grid items, so they occupy three stacked
rows. `place-items: center` centres each item *in its own row*; it does not put
them in one cell. A `<video>` with no `srcObject` has an intrinsic **300×150**
box, so it reserves ~150 px and the placeholder starts at a constant
`stage.top + 151` at *every* width. `.livetag` escaped this because it is
`position: absolute`; `.placeholder` never was.
2. **The stage's height is a function of its width with no floor and no ceiling.**
`aspect-ratio: 16 / 10` makes the box 180 px tall at 320 px wide while the
stacked content needs ~151 + 240 px, and `.stage { overflow: hidden }` (needed
for the rounded corners over live video) clips the remainder silently — with
no scroll container to reach it. On a short, wide window the same rule makes
the box *taller than the viewport* (543 px in a 480 px window), pushing the CTA
below the fold.
For scale, the placeholder's own content height measures **240 px at 320 px wide**
and **189 px at ≥480 px**.
## The live state is not affected (verified, keep it that way)
With a share running, `#placeholder` is `hidden`, the stack collapses, and
`#stop` measures clickable at 320×568, 390×844 and 1280×450 (`hit_stop: true`).
This phase must not change that.
## Dependencies
- `03_audio_share` (todo) — the preceding phase by number; no functional
dependency, but it and `01_keyboard_kiosk_control` both edit `app.css`. Keep
this phase's diff inside the `/* share */` block so the three never conflict.
## Boundaries (explicitly out of scope)
- No redesign of `/mirror`, no new markup for the stage, no JS/behaviour change —
`share.js` is not part of this fix.
- `/watch` is untouched: `#watch-stage` is `position: fixed; inset: 0` with no
`aspect-ratio`, and `.tile` genuinely wants its stacked rows.
- No breakpoint framework, no CSS preprocessor, no new dependency, no external
asset (PLAN A9/A10 are locked).
## Tasks
1. `01_one_cell_stage.md` — make the stage a single grid cell so the placeholder
overlays the video the way the markup intends.
2. `02_size_the_stage_for_real_viewports.md` — a floor and a ceiling for the box,
and an escape hatch so content taller than the stage can never be unreachable.
3. `03_reachable_cta_e2e.md` — the viewport sweep that proves the button is
clickable, idle and live.
4. `04_css_guard_and_docs.md` — the stylesheet guard that stops the two-row grid
coming back, and the documentation pass.
## Testing & Quality
- Unit/integration for all new logic: this phase adds no Python logic, so its
"unit" layer is a static stylesheet guard in `tests/test_app.py::TestStylesheet`
(regex over `app.css`, matching the style of `test_hidden_always_means_gone` and
`test_no_rule_reintroduces_a_conflicting_display_for_hidden`), and its real
layer is a browser suite.
- **`elementFromPoint` is the assertion**, not "the button has non-zero size":
`document.elementFromPoint(cx, cy).closest("#start")` must be the button. That
is the only in-browser statement that means *a finger or a cursor would hit it*,
and it is exactly the check that fails today.
- Success criterion: the **entire** project test suite passes 100% (every test,
not just this phase's — pre-existing failures included) with **>90% coverage**
on new/modified code, and all four gates from `AGENTS.md` §4 are green:
`uv run ruff check . && uv run ruff format --check .` · `uv run pyright` ·
`uv run pytest` · `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`.
- `tests/test_app.py::TestClientScripts` guards must stay green: no new element
ids, and the only cross-page ids remain `net`, `notice`, `toast`.
- No `ntfy` anywhere in this phase (it is outbound-only and cannot answer
questions).
## Documentation & Consistency (mandatory)
- Each task updates `README.md` wherever its own change is user-visible (here: a
narrow-screen/phone sharer).
- The phase's final pass reviews `README.md` **in its entirety** against the
project as it will stand after this phase and fixes every inconsistency found,
**including pre-existing ones not caused by this phase** (stale ports, commands
that no longer exist, a described layout that no longer matches).
- For `AGENTS.md` the final pass fixes **stale statements only** — a dead command,
a changed rule, an outdated fact — and **never adds or expands its content**.
- `PLAN.md` is not edited by this phase. Nothing here needs a new anchor: it is
plain CSS in the locked hand-written-frontend stack (A9/A10). `svh`/`dvh` units
and `align-content: safe center` are standard CSS with no dependency; the final
pass confirms both on the Firefox kiosk target (Firefox 101+ / ESR) and keeps a
`vh` fallback where the value is load-bearing.
## Completion Criteria
- [ ] `#start` passes the `elementFromPoint` hit test and is fully inside both the
stage box and the viewport at 320×568, 360×640, 390×844, 430×932, 480×800,
700×900, 900×480, 1024×600, 1280×450, 1280×800 and 1920×1080.
- [ ] `#stop` remains clickable at those sizes **while sharing** (unchanged
behaviour, asserted rather than assumed).
- [ ] `.stage` places its children in one grid cell: `grep`/regex guard shows no
plain-flow `.placeholder` sibling of the video, and `overflow: hidden` is
paired with a min-height or a scroll escape.
- [ ] Live video still fills the stage with rounded corners (no regression from
the single-cell change) and `#livetag` still sits top-left.
- [ ] `README.md` is accurate and consistent with the whole project; `AGENTS.md`
contains no stale statements (verified in the final pass); the full test
suite is 100% passing.
- [ ] No behavior change in phases 01–03: keyboard navigation, sharpness and
audio gates all still green.
@@ -0,0 +1,79 @@
# Task 01 — One cell, so the placeholder overlays the video
**Phase:** `04_reachable_share_cta` · **Source:** user request in chat — "the share button is cut off by the preview-stage"
**Story:** n/a
## Objective
Make `#preview-stage` a single grid cell. Today its children stack into rows, the
idle `<video>` reserves a ~150 px row (an empty `<video>` is intrinsically
300×150) and the placeholder — with the Share button in it — starts at
`stage.top + 151` at every viewport width and runs off the bottom into
`overflow: hidden`.
## Work
1. `src/mirror/static/app.css` — in the `.stage` rule (~L232–241) keep
`position: relative`, `aspect-ratio`, `background`, `border`,
`border-radius`, `overflow: hidden` and `place-items: center`, and make the
cell explicit:
```css
grid-template: 1fr / 1fr; /* one cell, not one row per child */
```
2. `src/mirror/static/app.css` — immediately after it, put every child in that
cell:
```css
.stage > * { grid-area: 1 / 1; }
```
so `#preview`, `#placeholder` and `#livetag` overlap instead of stacking.
DOM order already paints the placeholder above the video; do not add `z-index`.
3. `src/mirror/static/app.css` — leave `.livetag` as `position: absolute` (an
abspos child is out of flow, so `grid-area` is inert for it, and moving it into
the grid flow would be a visual change beyond this fix).
4. `src/mirror/static/app.css` — leave `.stage video { width: 100%; height: 100%;
object-fit: contain; }` alone: inside one cell it now means what it always
claimed to, and `object-fit: contain` keeps live video letterboxed with the
rounded corners intact.
5. `src/mirror/static/app.css` — add a comment at the rule stating the invariant
the next reader must not break: *the stage is one cell; a child added to
`#preview-stage` shares that cell and must never push another child out of
view.*
6. Do **not** touch `share.js` (`renderIdle()`/`renderLive()` already toggle
`#placeholder.hidden` correctly) and do not touch `mirror.html`.
ASSUMPTION: a single grid cell rather than `.placeholder { position: absolute;
inset: 0 }`. Both fix the stacking; the cell keeps the placeholder in flow (so its
height participates in layout, which task 02 needs for the floor/ceiling) and it
also fixes `#livetag`'s siblings for free.
ASSUMPTION: no change to the `.stage` border-radius or background — the corner
clipping over live video is intentional and is what `overflow: hidden` is for.
## Testing & Quality
- Manual/instrumented check while working (Chromium, `channel="chromium"`, over
`http://127.0.0.1:<port>`): at 320×568 and 1280×800 record
`#placeholder`'s offset from `#preview-stage` — it must no longer be the
constant 151 px, and `#start`'s bottom must be inside the stage at 1280×800
and closer to inside at 320×568 (task 02 finishes the narrow case).
- `uv run pytest tests/test_app.py` — the stylesheet and script guards stay
green; `[hidden] { display: none !important }` still beats the new
`grid-template` on `#placeholder` when it is hidden.
- Screenshot pair (idle, 1280×800 and 390×844) attached to the commit or written
into the commit message, so the visual change is reviewable.
- Coverage: **>90%** on new/modified code (CSS only; covered by the browser suite
in task 03 and the guard in task 04).
## Completion Criteria
- [ ] `.stage` declares a single explicit cell and `.stage > *` declares
`grid-area: 1 / 1`.
- [ ] `#placeholder` is centred over the video box rather than sitting below a
150 px empty-video row (verified in the browser, both idle and live).
- [ ] Live share still fills the stage, letterboxed, with rounded corners and
`#livetag` top-left — no visual regression.
- [ ] `uv run pytest tests/test_app.py` green; `uv run ruff check . &&
uv run ruff format --check . && uv run pyright` clean.
@@ -0,0 +1,88 @@
# Task 02 — Give the stage a floor, a ceiling, and an escape hatch
**Phase:** `04_reachable_share_cta` · **Source:** user request in chat — "cut off … in narrower view widths"
**Story:** n/a
## Objective
`aspect-ratio: 16 / 10` alone makes the stage 180 px tall at 320 px wide (the
placeholder needs 240 px there) and 543 px tall in a 480 px-high window (the CTA
ends up below the fold). Bound the box from both ends and make "content taller
than the stage" impossible to trap.
## Work
1. `src/mirror/static/app.css` — add to `.stage`:
```css
min-height: 264px; /* 240px of placeholder at 320px wide + slack */
max-height: min(70vh, 640px); /* fallback for browsers without svh */
max-height: min(70svh, 640px);
```
Keep `aspect-ratio: 16 / 10` — with a floor and a ceiling it becomes the
*preferred* shape rather than a hard derivation.
2. `src/mirror/static/app.css` — escape hatch so nothing can ever be unreachable
again, whichever way the geometry falls:
```css
.stage .placeholder { max-height: 100%; overflow: auto; }
```
and centre the cell with `align-content: safe center` (or `place-content: safe
center`) on `.stage`, so overflowing content aligns to the start instead of
being clipped in half by both edges — the classic centred-overflow trap that
hid half the button here.
3. `src/mirror/static/app.css` — at narrow widths stop asking for a wide-screen
shape at all: inside `@media (max-width: 900px)` (the existing single-column
breakpoint, ~L228) add `.stage { aspect-ratio: 4 / 3; }`, and
`@media (max-width: 480px) { .stage { aspect-ratio: auto; } }` so a phone gets
a stage as tall as its content rather than a letterbox with a button in the
clip. Keep `min-height` doing the real work in both cases.
4. `src/mirror/static/app.css` — trim the placeholder's own padding on small
screens (`@media (max-width: 480px) { .stage .placeholder { padding: 16px; } }`)
to buy back the 16 px, and leave `.btn.big` (measured 168×52) untouched — it is
already a comfortable ≥44 px target.
5. `src/mirror/static/app.css` — keep the whole diff inside the `/* share */`
block so it cannot collide with phase 01's focus rules or phase 03's audio
marks in the same file.
ASSUMPTION: `min-height: 264px` from the measured 240 px of placeholder content at
320 px wide, not from a round number. If a future copy change makes the idle
placeholder taller, the `overflow: auto` + `safe center` pair keeps it reachable
rather than clipped.
ASSUMPTION: `max-height: min(70svh, 640px)` — the preview is a *preview*; on a
1920×1080 desktop the aspect ratio alone wanted 964 px and pushed the Session
panel off the screen. The live video letterboxes inside it (`object-fit: contain`),
so nothing is lost but the crop is visible.
ASSUMPTION: `svh` with a preceding `vh` declaration (silent fallback where `svh`
is unsupported). The kiosk fleet is Firefox on Silverblue; both are supported
there, and the fallback means the ceiling is never load-bearing on an old browser.
ASSUMPTION: no `dvh`, because a mobile URL bar changing the stage height mid-share
is worse than a slightly conservative ceiling.
## Testing & Quality
- Browser check at the measured failure points (320×568, 360×640, 390×844,
480×800, 900×480, 1280×450): `#start`'s rect must be inside both the stage rect
and the viewport rect. Record before/after numbers in the commit message — the
"before" column is in `00_phase.md`.
- `uv run pytest tests/test_app.py::TestStylesheet` — `test_hidden_always_means_gone`
and `test_no_rule_reintroduces_a_conflicting_display_for_hidden` stay green with
the new `overflow`/`max-height` rules (reveal with opacity/overflow, never
re-`display` something `hidden`).
- `uv run pytest -m e2e` — the existing share tests
(`test_the_idle_share_page_offers_only_a_start`,
`test_a_shared_window_reaches_a_viewer_as_moving_pixels`) unchanged.
- Coverage: **>90%** on new/modified code.
## Completion Criteria
- [ ] `#start` is fully inside `#preview-stage` and inside the viewport at 320×568,
390×844 and 480×800 (the three clip cases), and fully above the fold at
900×480 and 1280×450 (the two fold cases).
- [ ] `.stage` has both a `min-height` and a `max-height`, and the placeholder has
a scroll escape.
- [ ] At 1920×1080 the Session panel is reachable without scrolling past a
964 px preview.
- [ ] `uv run pytest tests/test_app.py -m "not e2e"` green; gates clean.
@@ -0,0 +1,91 @@
# Task 03 — Prove the CTA is clickable at every width, idle and live
**Phase:** `04_reachable_share_cta` · **Source:** user request in chat — "I can't click it in narrower view widths"
**Story:** n/a
## Objective
A browser suite that fails today and passes after tasks 01–02, and that would
catch the same class of regression on any future `/mirror` layout change. The
assertion is a hit test, not a size check.
## Work
1. `tests/e2e/test_share_layout.py` — new file, `pytestmark = pytest.mark.e2e`,
reusing the `base_url` / `browser` / `start_sharing` fixtures and helpers from
`tests/e2e/conftest.py` (`channel="chromium"`, the fake-media-stream args, and
the page-over-`http://127.0.0.1` rule — `mediaDevices` is undefined elsewhere
and the page would render the insecure-origin notice instead of the button).
2. Parametrise the viewport sweep — exactly the shapes measured in
`00_phase.md`, so the two failure modes stay individually covered:
```python
VIEWPORTS = [
(320, 568), (360, 640), (390, 844), (430, 932), (480, 800), # the clip cases
(700, 900), (900, 700), (1024, 600), (1280, 800), (1920, 1080),
(900, 480), (1280, 450), # the below-the-fold cases
]
```
One page per shape (`browser.new_page(viewport=...)`) or one page driven
through `set_viewport_size` — either is fine; reload after each resize.
3. The probe (this is the code that reproduced the bug — keep it verbatim in the
test so the next reader can re-run the diagnosis):
```js
() => {
const b = document.querySelector("#start");
const s = document.querySelector("#preview-stage").getBoundingClientRect();
const r = b.getBoundingClientRect();
const hit = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);
return {
disabled: b.disabled,
in_stage: r.bottom <= s.bottom + 1 && r.top >= s.top - 1,
in_viewport: r.bottom <= innerHeight && r.right <= innerWidth && r.top >= 0,
hit_is_button: !!hit && !!hit.closest("#start"),
hit: hit ? hit.id || hit.tagName : null,
no_h_scroll: document.documentElement.scrollWidth <= innerWidth,
};
}
```
Assert all of `in_stage`, `in_viewport`, `hit_is_button`, `no_h_scroll`, and
`disabled is False` — a disabled button would make the hit test vacuous. On
failure the assertion message must include `hit`, which named the culprit
(`H2`, `LABEL`, `#name`, `#share-link`) during diagnosis.
4. Live-state sweep in the same file: `start_sharing(page)`, then at
320×568, 390×844 and 1280×450 assert `#stop` passes the same hit test,
`#placeholder` is `hidden`, `#livetag` is visible and inside the stage rect,
and the viewer side is untouched (leave that to the existing media tests — this
is layout, not pixels).
5. `tests/e2e/test_share_layout.py` — verify the test **fails before the fix**:
run it against the pre-phase CSS (e.g. `git stash` of tasks 01–02, or a
temporary revert) and paste the failing widths into the commit message. A
layout test that never failed is not believed.
ASSUMPTION: the sweep uses `elementFromPoint` rather than `page.click("#start")`
because clicking starts a real capture and each click would need teardown; the hit
test is the same claim with no side effects. Task 03 step 4 still exercises a real
`start_sharing()` once, so the interaction path is not untested.
ASSUMPTION: 320 px is the floor of "realistic". Below that the page is expected to
scroll, not to fit.
ASSUMPTION: `+/- 1px` tolerance on the containment comparisons for sub-pixel
layout at fractional device pixel ratios.
## Testing & Quality
- `uv run pytest -m e2e -k share_layout` — green after tasks 01–02, red before.
- `uv run pytest -m e2e` — whole browser suite green, including
`tests/e2e/test_keyboard_wall.py` if phase 01 has landed by then.
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` unaffected (no capture
change), but run it once in the phase's final pass.
- Coverage: **>90%** on new/modified code; this task is tests + nothing else.
## Completion Criteria
- [ ] `tests/e2e/test_share_layout.py` exists, is parametrised over the 12
viewports, and asserts the `elementFromPoint` hit test for `#start`.
- [ ] The same file asserts `#stop` is clickable while a share is live at three
of those shapes.
- [ ] The commit message records the pre-fix failure (which widths failed and what
`hit` reported).
- [ ] `uv run pytest` fully green; gates clean.
@@ -0,0 +1,76 @@
# Task 04 — Keep the two-row grid from coming back, and fix the docs
**Phase:** `04_reachable_share_cta` · **Source:** user request in chat — "the share button is cut off by the preview-stage"
**Story:** n/a
## Objective
A static stylesheet guard that fails loudly if `.stage` regresses into a stack of
rows or loses its floor, plus the phase's documentation pass.
## Work
1. `tests/test_app.py::TestStylesheet` — add a guard in the file's existing regex
style (see `test_hidden_always_means_gone`), something like
`test_the_share_stage_is_one_cell`:
* the `.stage { … }` rule contains `grid-template: 1fr / 1fr` (or an equivalent
explicit single cell) **and** a `.stage > *` rule with `grid-area: 1 / 1`;
* the reason goes in the docstring, because the test looks arbitrary: *the
stage's children overlap on purpose; in plain flow an idle `<video>` is
intrinsically 300×150 and pushes the Share button out of an
`overflow: hidden` box.*
2. `tests/test_app.py::TestStylesheet` — add
`test_the_stage_has_a_floor_and_a_ceiling`: the `.stage` rule contains both
`min-height` and `max-height`, and any rule that combines `overflow: hidden`
with `aspect-ratio` has a `min-height` in the same block. This is the general
statement of the bug — an aspect-derived box with no floor is a clip.
3. `tests/test_app.py::TestStylesheet::test_hidden_always_means_gone` — add
`.placeholder` to the list of components that set their own `display` and are
toggled with `hidden`, since tasks 01–02 give it `overflow`/`max-height` and it
is toggled by `renderIdle()`/`renderLive()`.
4. `README.md` — add the sharer-facing line where the pages/quickstart are
described: `/mirror` works down to a 320 px-wide viewport, the preview keeps
its shape but never shrinks below the controls it holds. Keep it user-facing;
no CSS internals in the README.
5. `README.md` **full consistency pass** (mandatory, phase-level): read the whole
file against the project as it stands after phases 01–04 and fix every
inconsistency, **including pre-existing ones this phase did not cause** — stale
ports or commands, a described layout that no longer matches, a
`MIRROR_MAX_BITRATE` default that phase 02 changes, keyboard behaviour phase 01
adds. Cite the file:line of each fix in the commit message.
6. `AGENTS.md` — fix **stale statements only** (a dead command, a changed rule, an
outdated fact), never add or expand content. If nothing is stale, say so in the
commit message rather than inventing an edit.
7. `.agents/PLAN.md` — not touched by this phase. If the pass finds a PLAN
statement contradicted by reality (e.g. §6's "default 12 Mbit/s"), report it in
the commit message and to the user; only they may amend it.
ASSUMPTION: the guards assert the *structure* (`grid-template` + `grid-area`)
rather than a pixel value, matching the existing stylesheet tests' approach —
pixel assertions belong in `tests/e2e/test_share_layout.py`, where a real browser
can answer them.
ASSUMPTION: no new CSS custom property for the stage floor. One value, one place,
and a variable would hide the number from the guard regexes.
## Testing & Quality
- `uv run pytest tests/test_app.py::TestStylesheet` — the new guards pass, and each
one fails when its CSS change is reverted (verify by hand; note it in the commit
message).
- `uv run pytest -m "not e2e"` fast suite green, then the whole suite including
`-m e2e` and `-m realdisplay` in the final pass.
- Coverage: **>90%** on new/modified code; the entire project suite is 100%
passing before this phase is called done (`AGENTS.md` §4).
## Completion Criteria
- [ ] Two new `TestStylesheet` guards exist, pass, and demonstrably fail when the
corresponding CSS is reverted.
- [ ] `.placeholder` is in the `[hidden]` component list.
- [ ] README states the narrow-screen behaviour and survives a full-file
consistency review with every finding fixed or explicitly reported.
- [ ] `AGENTS.md` has no stale statements and no new narrative.
- [ ] Full gate: `uv run ruff check . && uv run ruff format --check . && uv run
pyright && uv run pytest && MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay`.
- [ ] Committed per `AGENTS.md` §9: Conventional Commits, `--no-gpg-sign`,
`LLM-Generated: true` trailer.