diff --git a/skills/client-handover/resources/branding/zenquality.css b/skills/client-handover/resources/branding/zenquality.css
index 2f9e7c7..2d9f978 100644
--- a/skills/client-handover/resources/branding/zenquality.css
+++ b/skills/client-handover/resources/branding/zenquality.css
@@ -248,6 +248,25 @@ ul li, ol li { margin: 0 0 1.5mm 0; }
ul li::marker { color: var(--green-moss); }
ol li::marker { color: var(--green-moss); font-weight: 600; }
+/* ============ PAGE-BREAK HARDENING ============ */
+/* Keep each list item intact across pages — prevents the bullet/marker
+ from staying on the previous page while the text reflows to the next
+ (historical cause of "trailing word + leading bullet" superposition). */
+li {
+ page-break-inside: avoid;
+ break-inside: avoid;
+}
+/* Tie the first block after a heading to the heading itself so a page
+ break never splits "heading + intro" or "heading + first list item"
+ across two pages. */
+h1 + p, h1 + ul, h1 + ol,
+h2 + p, h2 + ul, h2 + ol,
+h3 + p, h3 + ul, h3 + ol,
+h4 + p, h4 + ul, h4 + ol {
+ page-break-before: avoid;
+ break-before: avoid;
+}
+
strong { color: var(--green-dark); font-weight: 600; }
em { color: var(--green-forest); font-style: italic; }
@@ -430,6 +449,17 @@ hr {
a[href^="#"]::after,
a[href^="mailto:"]::after,
a[href^="tel:"]::after,
+ a.bare-url::after,
.cover a::after,
table a::after { content: ""; }
+ /* Belt-and-braces: prevent the ::after URL pseudo-element from breaking
+ across pages or columns and overlapping the next block (root cause of
+ historical "text superposition" bugs on long URLs). */
+ a[href^="http"]::after {
+ white-space: nowrap;
+ page-break-before: avoid;
+ page-break-inside: avoid;
+ break-before: avoid;
+ break-inside: avoid;
+ }
}
diff --git a/skills/client-handover/scripts/handover-to-pdf.sh b/skills/client-handover/scripts/handover-to-pdf.sh
index a32db61..37ec249 100755
--- a/skills/client-handover/scripts/handover-to-pdf.sh
+++ b/skills/client-handover/scripts/handover-to-pdf.sh
@@ -146,6 +146,54 @@ print(markdown.markdown(
BODY_HTML="$(md_to_html_body "$SRC_MD")"
+# Tag anchors whose visible text equals their href (auto-linked bare URLs)
+# so the print stylesheet skips the "(href)" pseudo-element duplication.
+# Without this, "[https://x.com/](https://x.com/)" or a bare URL renders as
+# "https://x.com/ (https://x.com/)" and the trailing duplicate wraps onto
+# the next line, overlapping the following block.
+tag_bare_url_links() {
+ # Pass HTML via env var so the heredoc can be the python script.
+ HQ_RAW_HTML="$1" python3 <<'PY'
+import os, sys, re, html as html_lib
+
+src = os.environ.get("HQ_RAW_HTML", "")
+
+def normalize(u: str) -> str:
+ return html_lib.unescape(u).strip().rstrip('/').lower()
+
+# Match TEXT with no nested tags inside the anchor.
+ANCHOR_RE = re.compile(
+ r']*?)\bhref="([^"]+)"([^>]*)>([^<]*)',
+ flags=re.IGNORECASE,
+)
+
+def repl(m: re.Match) -> str:
+ pre_attrs, href, post_attrs, text = m.groups()
+ if normalize(href) != normalize(text):
+ return m.group(0)
+ attrs = (pre_attrs or "") + (post_attrs or "")
+ class_re = re.compile(r'\bclass="([^"]*)"', flags=re.IGNORECASE)
+ cm = class_re.search(attrs)
+ if cm:
+ existing = cm.group(1)
+ if "bare-url" in existing.split():
+ new_attrs = attrs
+ else:
+ new_attrs = class_re.sub(
+ f'class="{existing} bare-url"', attrs, count=1
+ )
+ else:
+ new_attrs = attrs.rstrip() + ' class="bare-url"'
+ return f'{text}'
+
+sys.stdout.write(ANCHOR_RE.sub(repl, src))
+PY
+}
+
+if command -v python3 >/dev/null 2>&1; then
+ BODY_HTML="$(tag_bare_url_links "$BODY_HTML")"
+fi
+
# ---------------------------- WRAP HTML ----------------------------
CSS_CONTENT="$(cat "$BRANDING_DIR/zenquality.css")"