diff --git a/client/src/components/SystemNotices/SystemNoticeModal.tsx b/client/src/components/SystemNotices/SystemNoticeModal.tsx
index 837bef10..82ae2189 100644
--- a/client/src/components/SystemNotices/SystemNoticeModal.tsx
+++ b/client/src/components/SystemNotices/SystemNoticeModal.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useLayoutEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import { flushSync } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { Info, AlertTriangle, AlertOctagon, X, ChevronLeft, ChevronRight } from 'lucide-react';
@@ -71,159 +71,168 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
: DefaultIcon;
return (
-
+
{/* Dismiss X button — only on last page so users read all notices */}
{notice.dismissible && isLastPage && (
)}
- {/* Hero image (not inline) */}
- {notice.media && notice.media.placement !== 'inline' && (
-
-

{ (e.target as HTMLImageElement).style.display = 'none'; }}
- />
-
- )}
-
- {/* Special warm header for Heart icon (thank-you notice) */}
- {notice.icon === 'Heart' && !notice.media && (
-
- )}
-
-
- {/* Severity icon (when no hero and not Heart) */}
- {!notice.media && notice.icon !== 'Heart' && (
-
-
-
- )}
-
- {/* Title (not for Heart — rendered in gradient header) */}
- {(notice.icon !== 'Heart' || notice.media) && (
-
- {title}
-
- )}
-
- {/* Body — markdown (long body text uses left-aligned layout) */}
-
-
{body}}>
- (
-
- {children}
-
- ),
- p: ({ children }) => {
- // Signature line styling (e.g. "— Maurice")
- const text = typeof children === 'string' ? children : Array.isArray(children) ? children.find(c => typeof c === 'string') : '';
- if (typeof text === 'string' && text.trim().startsWith('—') && text.trim().length < 30) {
- return {children}
;
- }
- return {children}
;
- },
- hr: () => (
-
- ),
- strong: ({ children }) => {children},
- ul: ({ children }) => ,
- ol: ({ children }) => {children}
,
- }}
- >
- {body}
-
-
-
-
- {/* Inline image */}
- {notice.media?.placement === 'inline' && (
+ {/* Scrollable content — vertically centered when shorter than available space */}
+
+ {/* Hero image (not inline) */}
+ {notice.media && notice.media.placement !== 'inline' && (

{ (e.target as HTMLImageElement).style.display = 'none'; }}
/>
)}
- {/* Highlights */}
- {notice.highlights && notice.highlights.length > 0 && (
-
- {notice.highlights.map((h, i) => {
- const HIcon: React.ElementType | null = h.iconName
- ? ((LucideIcons as Record)[h.iconName] as React.ElementType) ?? null
- : null;
- return (
- -
- {HIcon
- ?
- : ✓
- }
- {t(h.labelKey)}
-
- );
- })}
-
+ {/* Special warm header for Heart icon (thank-you notice) */}
+ {notice.icon === 'Heart' && !notice.media && (
+
)}
+
+ {/* Severity icon (when no hero and not Heart) */}
+ {!notice.media && notice.icon !== 'Heart' && (
+
+
+
+ )}
+
+ {/* Title (not for Heart — rendered in gradient header) */}
+ {(notice.icon !== 'Heart' || notice.media) && (
+
+ {title}
+
+ )}
+
+ {/* Body — markdown */}
+
+
{body}}>
+ (
+
+ {children}
+
+ ),
+ p: ({ children }) => {
+ // Signature line styling (e.g. "— Maurice")
+ const text = typeof children === 'string' ? children : Array.isArray(children) ? children.find(c => typeof c === 'string') : '';
+ if (typeof text === 'string' && text.trim().startsWith('—') && text.trim().length < 30) {
+ return {children}
;
+ }
+ return {children}
;
+ },
+ hr: () => (
+
+ ),
+ strong: ({ children }) => {children},
+ ul: ({ children }) => ,
+ ol: ({ children }) => {children}
,
+ }}
+ >
+ {body}
+
+
+
+
+ {/* Inline image */}
+ {notice.media?.placement === 'inline' && (
+
+

{ (e.target as HTMLImageElement).style.display = 'none'; }}
+ />
+
+ )}
+
+ {/* Highlights */}
+ {notice.highlights && notice.highlights.length > 0 && (
+
+ {notice.highlights.map((h, i) => {
+ const HIcon: React.ElementType | null = h.iconName
+ ? ((LucideIcons as Record)[h.iconName] as React.ElementType) ?? null
+ : null;
+ return (
+ -
+ {HIcon
+ ?
+ : ✓
+ }
+ {t(h.labelKey)}
+
+ );
+ })}
+
+ )}
+
+
+
+ {/* Sticky footer — pager + CTA, always anchored at the bottom of the slot */}
+
{/* Pager — dots, arrows, counter (only when multiple notices) */}
{total > 1 && (
-
+
@@ -247,7 +256,7 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
onClick={onNext}
disabled={!canPage || currentPage === total - 1}
aria-label={t('system_notice.pager.next')}
- className="p-1 rounded text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
+ className="px-2 py-1 rounded border border-slate-200 dark:border-slate-700 text-slate-500 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
@@ -262,17 +271,8 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
)}
{/* CTA + dismiss link */}
-
- {!isLastPage && total > 1 ? (
- /* Non-last page: "Next" button to advance through all notices */
-
- ) : ctaLabel ? (
+
+ {ctaLabel && isLastPage ? (
- ) : (
+ ) : (notice.dismissible || isLastPage) && (