diff --git a/themes/tsuki/assets/css/components.css b/themes/tsuki/assets/css/components.css
index 83a09b2..dda9d49 100644
--- a/themes/tsuki/assets/css/components.css
+++ b/themes/tsuki/assets/css/components.css
@@ -1,5 +1,24 @@
/* tsuki: components — post cards, post meta, terms, pagination, code copy */
+/* a11y: visible focus + skip-link */
+:focus-visible {
+ outline: 2px solid var(--tsuki-accent);
+ outline-offset: 2px;
+}
+.skip-link {
+ position: absolute;
+ inset-inline-start: var(--tsuki-space-3);
+ inset-block-start: -3rem;
+ z-index: 100;
+ padding: var(--tsuki-space-2) var(--tsuki-space-4);
+ background: var(--tsuki-bg);
+ color: var(--tsuki-fg);
+ border: 1px solid var(--tsuki-border);
+ border-radius: var(--tsuki-radius);
+ transition: var(--tsuki-transition);
+}
+.skip-link:focus { inset-block-start: var(--tsuki-space-3); text-decoration: none; }
+
/* post card */
.post-card {
padding-block: var(--tsuki-space-6);
@@ -170,6 +189,19 @@ pre:hover .code-copy,
.code-copy:focus-visible { opacity: 1; }
.code-copy:hover { color: var(--tsuki-fg); }
+/* related posts (single page) */
+.related-posts {
+ margin-block-start: var(--tsuki-space-12);
+ padding-block-start: var(--tsuki-space-6);
+ border-block-start: 1px solid var(--tsuki-border);
+}
+.related-heading {
+ font-size: var(--tsuki-fs-xl);
+ margin: 0 0 var(--tsuki-space-4);
+}
+.related-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0; }
+.related-list .post-card { padding-block: var(--tsuki-space-4); }
+
/* heading anchor */
.heading-anchor {
margin-inline-end: var(--tsuki-space-2);
diff --git a/themes/tsuki/assets/css/home.css b/themes/tsuki/assets/css/home.css
index 5466696..fd75056 100644
--- a/themes/tsuki/assets/css/home.css
+++ b/themes/tsuki/assets/css/home.css
@@ -79,7 +79,6 @@
border: 1px solid var(--tsuki-border);
border-radius: var(--tsuki-radius-lg);
transition: var(--tsuki-transition);
- view-transition-name: var(--tsuki-vt-name, none);
}
.project-card:hover {
border-color: var(--tsuki-accent);
diff --git a/themes/tsuki/assets/js/giscus-theme.js b/themes/tsuki/assets/js/giscus-theme.js
index 29271bb..9c5076c 100644
--- a/themes/tsuki/assets/js/giscus-theme.js
+++ b/themes/tsuki/assets/js/giscus-theme.js
@@ -12,5 +12,16 @@ if (giscus) {
"https://giscus.app"
);
};
+ // Push current theme as soon as Giscus iframe is ready so first paint matches.
+ // Self-remove after first valid Giscus message; later theme changes are caught by the MutationObserver below.
+ let primed = false;
+ const onFirstGiscusMessage = (e) => {
+ if (primed) return;
+ if (e.origin !== "https://giscus.app" || !e.data || !e.data.giscus) return;
+ primed = true;
+ window.removeEventListener("message", onFirstGiscusMessage);
+ send();
+ };
+ window.addEventListener("message", onFirstGiscusMessage);
new MutationObserver(send).observe(root, { attributes: true, attributeFilter: ["data-theme"] });
}
diff --git a/themes/tsuki/layouts/_markup/render-image.html b/themes/tsuki/layouts/_markup/render-image.html
new file mode 100644
index 0000000..91b6d4a
--- /dev/null
+++ b/themes/tsuki/layouts/_markup/render-image.html
@@ -0,0 +1,5 @@
+{{- /* In-content images get loading="lazy" + decoding="async" by default. */ -}}
+
+{{- /**/ -}}
diff --git a/themes/tsuki/layouts/_markup/render-link.html b/themes/tsuki/layouts/_markup/render-link.html
new file mode 100644
index 0000000..7b58237
--- /dev/null
+++ b/themes/tsuki/layouts/_markup/render-link.html
@@ -0,0 +1,11 @@
+{{- /* External markdown links get rel="noopener noreferrer". No target="_blank" — UX choice. */ -}}
+{{- $external := or
+ (strings.HasPrefix .Destination "http://")
+ (strings.HasPrefix .Destination "https://")
+ (strings.HasPrefix .Destination "//")
+-}}
+{{ .Text | safeHTML }}
+{{- /**/ -}}
diff --git a/themes/tsuki/layouts/_partials/header.html b/themes/tsuki/layouts/_partials/header.html
index 60d8836..a5683f5 100644
--- a/themes/tsuki/layouts/_partials/header.html
+++ b/themes/tsuki/layouts/_partials/header.html
@@ -1,6 +1,6 @@