diff --git a/apps/readest-app/src/__tests__/app/reader/components/Ribbon.test.tsx b/apps/readest-app/src/__tests__/app/reader/components/Ribbon.test.tsx
new file mode 100644
index 00000000..009d8627
--- /dev/null
+++ b/apps/readest-app/src/__tests__/app/reader/components/Ribbon.test.tsx
@@ -0,0 +1,30 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render } from '@testing-library/react';
+import Ribbon from '@/app/reader/components/Ribbon';
+
+vi.mock('@/store/themeStore', () => ({
+ useThemeStore: () => ({ safeAreaInsets: { top: 48, right: 0, bottom: 0, left: 0 } }),
+}));
+
+describe('Ribbon', () => {
+ it('stacks above the scrolled-mode notch mask so the full ribbon stays visible', () => {
+ // In scrolled mode SectionInfo paints a `bg-base-100` `notch-area` mask over the
+ // top safe-area strip at z-10. The ribbon renders earlier in the DOM, so it must
+ // sit on a higher layer than z-10 or its upper (unsafe-area) half gets covered.
+ const { container } = render();
+ const ribbon = container.querySelector('.ribbon') as HTMLElement;
+
+ expect(ribbon).not.toBeNull();
+ expect(ribbon.classList.contains('z-10')).toBe(false);
+ expect(ribbon.classList.contains('z-20')).toBe(true);
+ // Decorative only: taps must fall through to the notch mask's scroll-to-top.
+ expect(ribbon.classList.contains('pointer-events-none')).toBe(true);
+ });
+
+ it('spans the safe-area inset plus the header bar height', () => {
+ const { container } = render();
+ const ribbon = container.querySelector('.ribbon') as HTMLElement;
+
+ expect(ribbon.style.height).toBe('92px'); // 48px safe-area top + 44px header bar
+ });
+});
diff --git a/apps/readest-app/src/app/reader/components/Ribbon.tsx b/apps/readest-app/src/app/reader/components/Ribbon.tsx
index 440c5223..cf8055b0 100644
--- a/apps/readest-app/src/app/reader/components/Ribbon.tsx
+++ b/apps/readest-app/src/app/reader/components/Ribbon.tsx
@@ -9,9 +9,13 @@ interface RibbonProps {
const Ribbon: React.FC = ({}) => {
const { safeAreaInsets } = useThemeStore();
+ // z-20 keeps the ribbon above the scrolled-mode `notch-area` mask (z-10 in
+ // SectionInfo) so its upper safe-area half isn't covered.
return (