diff --git a/CLAUDE.md b/CLAUDE.md
index 3b3a32b..7143046 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -235,6 +235,38 @@ Original-Zurueck/Weiter-Links, das Sheet verschwindet.
einer verschachtelten Tabelle, `closest('tr')` trifft die innere Zeile ohne
Autor- und Profilspalte.
+**Der Umbau erbt die Scrollposition, die der Browser vorher gesetzt hat.**
+`replaceBody()` laeuft auf `DOMContentLoaded` und ruehrt `scrollY` nicht an;
+weil ein Thread auch danach laenger als der Viewport bleibt, klemmt der Browser
+sie nie auf 0 zurueck. Auf dem iPhone startete der Thread deshalb mitten im
+ersten Beitrag: iOS Safari stellt die Position einer schon besuchten Adresse
+von sich aus wieder her und verankert sie ausserdem beim Relayout auf
+`width=device-width`. Im Chrome-Debugging faellt das nicht auf, weil dort jede
+Adresse frisch geladen wird und es gar keine Position zu uebernehmen gibt. Zwei
+Bremsen dagegen: `history.scrollRestoration = 'manual'` direkt beim
+Script-Start — spaeter waere es zu spaet, Safari hat dann laengst gescrollt —
+und `holdStart()` am Ende von `run()`.
+
+Ein einzelner Reset auf `DOMContentLoaded` reichte nicht — am Geraet
+widerlegt: Safari legt die Position nicht in einem Rutsch fest, sondern
+springt nach `load` und nach dem Layout nachgeladener Bilder noch einmal nach
+und ueberschreibt den Reset. `holdStart()` haelt den Anfang deshalb fest —
+zwoelf Versuche im 50-ms-Takt plus einer auf `load` fuer den Sprung, der erst
+nach dem letzten Bild kommt. Die Bremse loest bei der ersten eigenen Geste des
+Lesers aus (`touchstart`/`wheel`/`keydown`, jeweils `once`), sonst risse sie
+ihn mitten im Lesen zurueck. `scroll` taugt als Signal dafuer nicht:
+`toStart()` loest selbst eines aus und wuerde sich sofort selbst abschalten.
+
+**Die Post-Anker der Originalseite muessen auf die Karten wandern.** Ein
+Beitrag traegt sein Sprungziel als `` in der Autorenzelle, und
+die verschwindet mit dem Original-DOM. Bis 3.14 hatte `#pXXXX` deshalb kein
+Ziel mehr — der „Neueste ↓"-Link aus der Themenliste landete nicht am Beitrag,
+sondern dort, wo der Browser beim Umbau zufaellig stand. `buildThread()`
+uebernimmt den Namen jetzt als `id` der `.rm-post`-Karte, und `toStart()`
+springt darauf statt an den Anfang, wenn die Adresse ein Fragment traegt.
+`scroll-margin-top` auf `.rm-post` haelt dabei die fixierte Brotkrumenleiste
+frei, sonst steht der Kartenkopf dahinter.
+
**Die Lupe und die Login-Pille haengen an einer gemeinsamen `margin-left:auto`
auf einem Wrapper (`.rm-auth-group`), nicht je an sich selbst.** Saesse die
Regel wie zuvor direkt auf `.rm-auth`, wuerde nur die Pille an den Rand
@@ -368,6 +400,19 @@ Am Beitrag selbst zu pruefen: alles hinter dem ersten `
` im `postbody`
landet als zugeklapptes `details` („Signatur") am Kartenfuss, Beitraege ohne
`
` behalten ihren Text vollstaendig im Koerper.
+Fuer den Sprung an den Threadanfang genuegt derselbe Fixture zweimal, einmal
+mit und einmal ohne Fragment in der Adresse: ohne Fragment darf nur
+`scrollTo(0, 0)` fallen, mit `#pXXXX` nur ein `scrollIntoView()` auf genau der
+Karte mit dieser `id` — und jede der 15 Karten muss eine tragen. Fuer
+`holdStart()` kommen drei zeitabhaengige Faelle dazu: dass ohne Zutun weitere
+Resets nachkommen, dass ein abgesetztes `touchstart` sie sofort stoppt, und
+dass `load` nach dem Ende der Schleife noch einmal greift, aber nach einer
+eigenen Geste eben nicht mehr. Achtung: jsdom
+kennt `history.scrollRestoration` nicht, das Script laeuft dort stumm in seinen
+Rueckfall. Wer den Zweig pruefen will, definiert die Eigenschaft vor `eval()`
+selbst auf `window.history`. `scrollTo`, `scrollIntoView` und
+`requestAnimationFrame` fehlen ohnehin und muessen gestubbt werden.
+
Dieselben Faelle gelten seit 3.5 fuer die Liste, die sich die Leiste mit dem
Thread teilt. Eckdaten von `dev/viewforum.php`: `f=10`, `per_page = 100`, 610
Seiten, letzte Seite `start=60900`, dazu 4 Unterforen, 7 globale
diff --git a/rin-mobile.user.js b/rin-mobile.user.js
index 8d11b6b..e1c1c0d 100644
--- a/rin-mobile.user.js
+++ b/rin-mobile.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name RIN Mobile
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
-// @version 3.14
+// @version 3.15
// @match *://cs.rin.ru/forum/*
// @run-at document-start
// ==/UserScript==
@@ -60,6 +60,12 @@
: '';
log.push('ansicht=' + (view || 'keine'));
+ // Muss zum fruehestmoeglichen Zeitpunkt stehen, nicht erst beim Aufbau: iOS
+ // Safari stellt die Scrollposition einer schon besuchten Adresse von sich aus
+ // wieder her, und zwar bevor das Script ueberhaupt etwas gebaut hat. Ein
+ // spaeterer Reset kaeme dagegen nur hinterher.
+ if (view && 'scrollRestoration' in history) history.scrollRestoration = 'manual';
+
if (view && !document.querySelector('meta[name="viewport"][data-rm]')) {
var m = document.createElement('meta');
m.name = 'viewport';
@@ -237,9 +243,13 @@
font-size:21px; font-weight:650; line-height:1.3;
overflow-wrap:break-word; }
+ /* Die Karte traegt seit dem Anker-Uebertrag die id des Beitrags und ist
+ damit Sprungziel von #pXXXX. Ohne den Freiraum landet sie hinter der
+ fixierten Brotkrumenleiste, der Kartenkopf waere verdeckt. */
.rm-post { box-sizing:border-box !important;
width:100% !important; max-width:100% !important;
padding:14px !important;
+ scroll-margin-top:calc(53px + env(safe-area-inset-top));
border-top:1px solid rgba(128,128,128,.25) !important; }
.rm-head { display:flex; align-items:center; gap:9px;
@@ -1502,6 +1512,16 @@
pr = pr.previousElementSibling;
}
+ // --- Sprungziel ---
+ // Der Anker eines Beitrags steht als in der Autorenzelle
+ // und wandert mit dem Original-DOM in die Unsichtbarkeit. Ohne ihn auf der
+ // Karte hat eine Adresse mit #pXXXX kein Ziel mehr, und der Browser bleibt
+ // stehen, wo er beim Umbau gerade stand.
+ var anc = null;
+ for (var ci = 0; ci < cells.length && !anc; ci++)
+ anc = cells[ci].querySelector('a[name^="p"]');
+ if (anc) card.id = anc.getAttribute('name');
+
// --- Username ---
var user = '';
for (var i = 0; i < cells.length && !user; i++) {
@@ -1694,6 +1714,46 @@
// ==========================================================================
+ // Der Umbau laeuft erst auf DOMContentLoaded, also lange nachdem der Browser
+ // seine Scrollposition festgelegt hat — und replaceBody() ruehrt sie nicht an.
+ // Weil ein Thread auch nach dem Umbau laenger als der Viewport bleibt, klemmt
+ // der Browser sie auch nicht auf 0 zurueck: der Thread startet dann mitten im
+ // ersten Beitrag statt an seinem Anfang. Auf dem Geraet faellt das auf, im
+ // Chrome-Debugging nicht, weil dort jede Adresse frisch geladen wird und es
+ // gar keine Position zu uebernehmen gibt.
+ function toStart() {
+ var id = location.hash ? location.hash.slice(1) : '';
+ var target = id ? document.getElementById(id) : null;
+ if (target) target.scrollIntoView();
+ else window.scrollTo(0, 0);
+ }
+
+ // Einmal zuruecksetzen genuegt nicht: Safari legt die Position nicht in einem
+ // Rutsch fest, sondern springt nach 'load' und nach dem Layout nachgeladener
+ // Bilder noch einmal nach — ein Reset auf DOMContentLoaded wird davon einfach
+ // ueberschrieben. Der Anfang wird deshalb kurz festgehalten. Die Bremse loest
+ // bei der ersten eigenen Geste des Lesers sofort aus, sonst risse sie ihn
+ // waehrend des Lesens zurueck; 'scroll' taugt dafuer nicht als Signal, weil
+ // toStart() selbst eines ausloest.
+ function holdStart() {
+ var free = false;
+ function release() { free = true; }
+ ['touchstart', 'wheel', 'keydown'].forEach(function (ev) {
+ window.addEventListener(ev, release, { passive: true, once: true });
+ });
+
+ var tries = 0;
+ (function tick() {
+ if (free) return;
+ toStart();
+ if (++tries < 12) setTimeout(tick, 50);
+ })();
+
+ // Faengt den Sprung ab, der erst nach dem letzten Bild kommt und damit
+ // jenseits der 600 ms der Schleife liegen kann.
+ window.addEventListener('load', function () { if (!free) toStart(); });
+ }
+
function run() {
try {
if (view === 'liste') buildList();
@@ -1701,6 +1761,8 @@
else if (view === 'login') buildLogin();
} catch (e) { log.push('build: ' + e.message); }
+ if (view && document.querySelector('.rm-root')) holdStart();
+
if (DEBUG) {
var d = document.createElement('div');
d.className = 'rm-dbg';