diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md
index a8b3bdbc..8f1a6bb7 100644
--- a/apps/readest-app/.claude/memory/MEMORY.md
+++ b/apps/readest-app/.claude/memory/MEMORY.md
@@ -13,6 +13,7 @@
- [Reading ruler line/column-aware](reading-ruler-line-aware.md) — ruler snaps to real lines; multi-column band spans one column; Range.getClientRects() returns tall block boxes that must be dropped; iframe frame-offset mapping; synthetic-key throttling
- [TOC expand + auto-scroll](toc-expand-and-autoscroll.md) — #4059 collapse-by-default policy in `tocTree.ts`; pinned-sidebar mounts before progress → dynamic expansion breaks scroll-to-current via (1) spurious onScroll clearing pending and (2) Virtuoso scrollToIndex landing short after row growth (re-assert on rAF)
- [Swipe page-turn bg flash](paginator-swipe-bg-flash.md) — white↔black flash on swipe+animation only; `#background` was static screen-space and didn't track content during drag/snap; fix = sliding per-view full-bleed segments (`computeBackgroundSegments`) rebuilt on scroll + per-rAF synced to the view transform during snap
+- [Duokan fullscreen cover hidden in scroll mode](duokan-fullscreen-cover-scroll.md) — #4379 `data-duokan-page-fullscreen` cover pinned `position:absolute height:100%` collapses against auto-height scroll container; gate fullscreen on `this.#column` + reset stale absolute props on toggle (`setImageSize` in paginator.js)
## Critical Files (Most Bug-Prone)
- `src/utils/style.ts` - Central EPUB CSS transformation hub (14+ bug fixes)
diff --git a/apps/readest-app/.claude/memory/duokan-fullscreen-cover-scroll.md b/apps/readest-app/.claude/memory/duokan-fullscreen-cover-scroll.md
new file mode 100644
index 00000000..cfe68395
--- /dev/null
+++ b/apps/readest-app/.claude/memory/duokan-fullscreen-cover-scroll.md
@@ -0,0 +1,21 @@
+---
+name: duokan-fullscreen-cover-scroll
+description: "Duokan fullscreen cover image invisible in scrolled mode (#4379) — paginator pins it position:absolute height:100% which collapses against auto-height scroll container"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: c45aabf0-e8a3-42b6-a5fd-c04d6eb2345c
+---
+
+Issue #4379: an EPUB cover with `data-duokan-page-fullscreen` on `` (Duokan/DangDang convention) shows in paginated mode but is **blank in scrolled mode**; only the first cover image, other images fine in both modes.
+
+**Root cause** — `View.setImageSize()` in `packages/foliate-js/paginator.js` has a `pageFullscreen` branch that pins each img with `position:absolute; inset:0; width:100%; height:100%` and forces ancestors + `` to `height:100%`/`position:relative`. This fills the page in paginated/columnized mode (html has a fixed pixel height). In scrolled mode `scrolled()` sets `html`/`body` height to `auto`, so the `height:100%` chain resolves to **0** and the absolutely-positioned cover collapses out of view (offsetHeight 0).
+
+**Fix** — gate the fullscreen treatment on column mode: `const applyFullscreen = pageFullscreen && this.#column`. Use `applyFullscreen` for the max-height/max-width margin term and the `if` block. Add an `else if (pageFullscreen)` that `removeProperty`s the stale `position/inset/width/height/margin` on the img (and `width/height/margin/padding` on ancestors, `position` on html) so toggling paginated→scrolled doesn't leave the cover collapsed (same iframe/img is reused via `view.render(layout)` on `flow` change). In scrolled mode the cover then flows like a normal full-page image bounded by `max-height = availableHeight`.
+
+**Key facts**
+- `this.#column = layout.flow !== 'scrolled'` (set in `render()` before `setImageSize`), so it's reliable inside `setImageSize`.
+- Foliate writes these styles as **inline `!important`** → cannot be overridden from `src/utils/style.ts`; the fix must live in the paginator.
+- Regression test: `src/__tests__/document/paginator-duokan-cover.browser.test.ts` + fixture `repro-4379.epub` (cover xhtml with the duokan attr + dimensionless `
`). Asserts cover `img.offsetHeight > 0` in scrolled mode, paginated sanity, and paginated→scrolled toggle. Browser test (real layout) is required — jsdom can't compute the collapse.
+
+Related: [[paginator-swipe-bg-flash]], [[css-style-fixes]].
diff --git a/apps/readest-app/src/__tests__/document/paginator-duokan-cover.browser.test.ts b/apps/readest-app/src/__tests__/document/paginator-duokan-cover.browser.test.ts
new file mode 100644
index 00000000..05938081
--- /dev/null
+++ b/apps/readest-app/src/__tests__/document/paginator-duokan-cover.browser.test.ts
@@ -0,0 +1,150 @@
+import { describe, it, expect, beforeAll, afterEach } from 'vitest';
+import { DocumentLoader } from '@/libs/document';
+import type { BookDoc } from '@/libs/document';
+import type { Renderer } from '@/types/view';
+
+// repro-4379: a Duokan/DangDang full-page cover — `data-duokan-page-fullscreen`
+// on with a dimensionless
inside a `.cover` wrapper. The paginator
+// pins such covers with `position:absolute; inset:0; height:100%`, which fills
+// the fixed-height page in paginated mode but collapses to zero against the
+// auto-height scroll container in scrolled mode (the cover disappears).
+const EPUB_URL = new URL('../fixtures/data/repro-4379.epub', import.meta.url).href;
+
+let book: BookDoc;
+
+const loadEPUB = async () => {
+ const resp = await fetch(EPUB_URL);
+ const buffer = await resp.arrayBuffer();
+ const file = new File([buffer], 'repro-4379.epub', { type: 'application/epub+zip' });
+ const loader = new DocumentLoader(file);
+ const { book } = await loader.open();
+ return book;
+};
+
+const waitForStabilized = (el: HTMLElement, timeout = 10000) =>
+ new Promise((resolve, reject) => {
+ const timer = setTimeout(() => reject(new Error('stabilized timeout')), timeout);
+ el.addEventListener(
+ 'stabilized',
+ () => {
+ clearTimeout(timer);
+ resolve();
+ },
+ { once: true },
+ );
+ });
+
+const getCoverImg = (paginator: Renderer): HTMLImageElement | null => {
+ const cover = paginator.getContents().find((c) => c.index === 0);
+ return (cover?.doc.body.querySelector('img') as HTMLImageElement | undefined) ?? null;
+};
+
+/** Wait for the cover
resource to decode so layout has a natural size. */
+const waitForImgLoaded = async (img: HTMLImageElement, timeout = 5000) => {
+ const start = Date.now();
+ while (Date.now() - start < timeout) {
+ if (img.complete && img.naturalHeight > 0) return;
+ await new Promise((r) => setTimeout(r, 50));
+ }
+};
+
+/** Poll until the element has a non-zero rendered height (or time out). */
+const waitForVisibleHeight = async (img: HTMLImageElement, timeout = 3000) => {
+ const start = Date.now();
+ while (Date.now() - start < timeout) {
+ if (img.offsetHeight > 0) return;
+ await new Promise((r) => setTimeout(r, 50));
+ }
+};
+
+describe('Paginator Duokan fullscreen cover (#4379)', () => {
+ let paginator: Renderer;
+
+ beforeAll(async () => {
+ book = await loadEPUB();
+ await import('foliate-js/paginator.js');
+ }, 30000);
+
+ const createPaginator = () => {
+ const el = document.createElement('foliate-paginator') as Renderer;
+ Object.assign(el.style, {
+ width: '800px',
+ height: '600px',
+ position: 'absolute',
+ left: '0',
+ top: '0',
+ });
+ document.body.appendChild(el);
+ return el;
+ };
+
+ afterEach(() => {
+ if (paginator) {
+ try {
+ paginator.destroy();
+ } catch {
+ /* iframe body may already be torn down */
+ }
+ paginator.remove();
+ }
+ });
+
+ it('shows the cover image in paginated mode (sanity)', async () => {
+ paginator = createPaginator();
+ paginator.open(book);
+
+ const stabilized = waitForStabilized(paginator);
+ await paginator.goTo({ index: 0 });
+ await stabilized;
+
+ const img = getCoverImg(paginator);
+ expect(img).toBeTruthy();
+ await waitForImgLoaded(img!);
+ await waitForVisibleHeight(img!);
+ expect(img!.offsetHeight).toBeGreaterThan(0);
+ });
+
+ it('shows the cover image in scrolled mode', async () => {
+ paginator = createPaginator();
+ paginator.open(book);
+ paginator.setAttribute('flow', 'scrolled');
+
+ const stabilized = waitForStabilized(paginator);
+ await paginator.goTo({ index: 0 });
+ await stabilized;
+
+ const img = getCoverImg(paginator);
+ expect(img).toBeTruthy();
+ await waitForImgLoaded(img!);
+ await waitForVisibleHeight(img!);
+
+ // Before the fix the cover is absolutely positioned at height:100% inside
+ // an auto-height (zero) container, so it collapses and never renders.
+ expect(img!.offsetHeight).toBeGreaterThan(0);
+ // It must also stay bounded by the viewport rather than overflowing it.
+ expect(img!.offsetHeight).toBeLessThanOrEqual(600);
+ });
+
+ it('shows the cover image after toggling paginated -> scrolled', async () => {
+ paginator = createPaginator();
+ paginator.open(book);
+
+ const stabilized = waitForStabilized(paginator);
+ await paginator.goTo({ index: 0 });
+ await stabilized;
+
+ const img = getCoverImg(paginator);
+ expect(img).toBeTruthy();
+ await waitForImgLoaded(img!);
+ await waitForVisibleHeight(img!);
+
+ const stabilized2 = waitForStabilized(paginator);
+ paginator.setAttribute('flow', 'scrolled');
+ await stabilized2;
+
+ // The same
element is re-rendered; stale absolute positioning from
+ // the paginated render must not leave it collapsed in scrolled mode.
+ await waitForVisibleHeight(img!);
+ expect(img!.offsetHeight).toBeGreaterThan(0);
+ });
+});
diff --git a/apps/readest-app/src/__tests__/fixtures/data/repro-4379.epub b/apps/readest-app/src/__tests__/fixtures/data/repro-4379.epub
new file mode 100644
index 00000000..5592f758
Binary files /dev/null and b/apps/readest-app/src/__tests__/fixtures/data/repro-4379.epub differ
diff --git a/packages/foliate-js b/packages/foliate-js
index 167757a1..e8995dae 160000
--- a/packages/foliate-js
+++ b/packages/foliate-js
@@ -1 +1 @@
-Subproject commit 167757a1020a77ef0cbd8f7b291a47e5ccc83186
+Subproject commit e8995daeb6f7743aae4ac617529a3ce2f076dfab