diff --git a/assets/layout.html b/assets/layout.html
index 06dcb7c..b6ae07f 100644
--- a/assets/layout.html
+++ b/assets/layout.html
@@ -11,6 +11,7 @@
+
diff --git a/assets/scroll-sync.js b/assets/scroll-sync.js
new file mode 100644
index 0000000..b17326b
--- /dev/null
+++ b/assets/scroll-sync.js
@@ -0,0 +1,61 @@
+// Roughly preserve reading position when entering the editor.
+//
+// While reading a page we record how far the center column is scrolled (as a
+// fraction of its scrollable height, keyed by path). When the same path opens
+// in the editor we restore that fraction, so you land near where you were
+// reading instead of at the top. It is fraction-based — the rendered page and
+// the raw-markdown editor have different heights — so the match is deliberately
+// approximate. This only moves the viewport; the caret default (end of doc) is
+// unchanged, so a click still decides where you actually edit.
+(function () {
+ var center = document.querySelector('.center');
+ if (!center) return;
+
+ var key = 'scrollsync:' + location.pathname;
+ // Only restore a freshly-recorded position: a stale entry from earlier in
+ // the tab session would jump a direct ?edit open to the wrong place.
+ var MAX_AGE_MS = 5 * 60 * 1000;
+ var params = new URLSearchParams(location.search);
+ var inEditor = params.has('edit');
+ // Section / insert edits load only a slice of the document into the editor,
+ // so a whole-page reading fraction does not map onto them — restore only for
+ // a full-page ?edit.
+ var fullPageEdit = inEditor && !params.has('section') && !params.has('insert_before');
+
+ function maxScroll() { return Math.max(0, center.scrollHeight - center.clientHeight); }
+
+ if (inEditor) {
+ if (!fullPageEdit) return;
+ var raw = sessionStorage.getItem(key);
+ if (!raw) return;
+ var data;
+ try { data = JSON.parse(raw); } catch (e) { return; }
+ if (!data || typeof data.f !== 'number' || Date.now() - (data.t || 0) > MAX_AGE_MS) return;
+ // Wait for CodeMirror to mount and the column to reach full height, then
+ // map the fraction onto it. A double rAF after load lets CM's initial
+ // layout and focus scroll settle so ours wins.
+ window.addEventListener('load', function () {
+ requestAnimationFrame(function () {
+ requestAnimationFrame(function () {
+ center.scrollTop = data.f * maxScroll();
+ });
+ });
+ });
+ return;
+ }
+
+ // Reading mode: keep the latest scroll fraction recorded (throttled to one
+ // write per frame) so it is ready the moment the editor opens.
+ var pending = false;
+ center.addEventListener('scroll', function () {
+ if (pending) return;
+ pending = true;
+ requestAnimationFrame(function () {
+ pending = false;
+ var m = maxScroll();
+ try {
+ sessionStorage.setItem(key, JSON.stringify({ f: m > 0 ? center.scrollTop / m : 0, t: Date.now() }));
+ } catch (e) {}
+ });
+ }, { passive: true });
+})();