@@ -8,6 +8,11 @@ interface Props {
88 /** Absolute path under /public, e.g. "/og/hello-world.png". */
99 image? : string ;
1010 mode? : ' ink' | ' paper' ;
11+ /** Override portrait-mobile stage height. Any valid CSS length; default
12+ 30svh. Pages whose stage wants more breathing room (e.g. 404) pass a
13+ larger value. Feeds a CSS variable consumed by the mobile media
14+ query below — desktop layout isn't affected. */
15+ stageHeightMobile? : string ;
1116}
1217const SITE_NAME = ' 眠海' ;
1318const DEFAULT_DESCRIPTION = ' 眠海 / SUNKEN KEEP — 生成式与交互作品的陈列。' ;
@@ -16,14 +21,15 @@ const {
1621 description = DEFAULT_DESCRIPTION ,
1722 image,
1823 mode = ' ink' ,
24+ stageHeightMobile = ' 30svh' ,
1925} = Astro .props ;
2026const fullTitle = title === SITE_NAME ? SITE_NAME : ` ${title } · ${SITE_NAME } ` ;
2127const canonicalURL = new URL (Astro .url .pathname , Astro .site );
2228const ogImage = image ? new URL (image , Astro .site ).toString () : undefined ;
2329---
2430
2531<!DOCTYPE html >
26- <html lang =" zh-CN" class:list ={ [mode === ' paper' && ' paper' ]} >
32+ <html lang =" zh-CN" class:list ={ [mode === ' paper' && ' paper' ]} style = { ` --stage-h-mobile: ${ stageHeightMobile } ` } >
2733 <head >
2834 <meta charset =" utf-8" />
2935 <meta name =" viewport" content =" width=device-width, initial-scale=1" />
@@ -127,7 +133,16 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
127133
128134 const dismiss = () => {
129135 try { localStorage .setItem (KEY , ' 1' ); } catch {}
130- document .body .classList .remove (' show-rotate-hint' );
136+ const body = document .body ;
137+ if (! body .classList .contains (' show-rotate-hint' )) return ;
138+ // Run the leave animation, then drop both classes. display:flex
139+ // stays active (via .show-rotate-hint) throughout the fade, so
140+ // the overlay remains visible while opacity animates to 0.
141+ body .classList .add (' rotate-hint-leaving' );
142+ window .setTimeout (() => {
143+ body .classList .remove (' show-rotate-hint' );
144+ body .classList .remove (' rotate-hint-leaving' );
145+ }, 460 );
131146 };
132147
133148 const wire = () => {
@@ -365,8 +380,10 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
365380 position : sticky ;
366381 top : 0 ;
367382 /* svh = small viewport height, ignores dynamic URL bar on mobile.
368- Without it, heights jump when Safari collapses its chrome. */
369- height : 30svh ;
383+ Without it, heights jump when Safari collapses its chrome.
384+ Defaults to 30svh; any page passing stageHeightMobile to
385+ BaseLayout overrides via the --stage-h-mobile root var. */
386+ height : var (--stage-h-mobile, 30svh);
370387 z - index : 1 ;
371388 border - right : none ;
372389 /* Hard 1px rule replaced by the body::before fade strip below,
@@ -401,7 +418,7 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
401418 z - index : 2 ;
402419 }
403420 body ::before {
404- top : calc (30svh - 0.8em );
421+ top : calc (var ( -- stage - h - mobile , 30svh ) - 0.8em );
405422 height : 3.3em ;
406423 background : linear - gradient (
407424 to bottom ,
@@ -448,6 +465,16 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
448465 @media (max-width: 860px) and (orientation: portrait) {
449466 body .show - rotate - hint .rotate - hint { display : flex ; }
450467 }
468+ /* Fade-out on dismiss. JS adds .rotate-hint-leaving before removing
469+ .show-rotate-hint after the animation finishes, so display:flex
470+ stays active while opacity animates to 0. */
471+ @keyframes rotate-hint-leave {
472+ from { opacity : 1 ; }
473+ to { opacity : 0 ; }
474+ }
475+ body.rotate-hint-leaving .rotate-hint {
476+ animation : rotate - hint - leave 460ms ease - out both ;
477+ }
451478 .rotate-hint-card {
452479 text - align : center ;
453480 color : var (--fg);
@@ -459,10 +486,13 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
459486 color : var (--accent);
460487 animation : rotate - hint - turn 3.6s cubic - bezier (0.65 , 0 , 0.35 , 1 ) infinite ;
461488 }
489+ /* Loop: hold portrait, rotate to landscape, hold, rotate back,
490+ hold briefly, repeat. The end state matches the start so the
491+ infinite cycle is seamless instead of snapping back. */
462492 @keyframes rotate-hint-turn {
463- 0 % , 20 % { transform : rotate (0deg ); }
464- 45 % , 70 % { transform : rotate (- 90deg ); }
465- 95 % , 100 % { transform : rotate (- 90 deg ); }
493+ 0 % , 15 % { transform : rotate (0deg ); }
494+ 40 % , 65 % { transform : rotate (- 90deg ); }
495+ 90 % , 100 % { transform : rotate (0 deg ); }
466496 }
467497 .rotate-hint-text {
468498 font - family : var (--font-body);
@@ -483,6 +513,7 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
483513 @media (prefers-reduced-motion: reduce) {
484514 .rotate - hint { animation : none ; }
485515 .rotate - hint - icon { animation : none ; transform : rotate (- 90deg ); }
516+ body .rotate - hint - leaving .rotate - hint { animation : none ; opacity : 0 ; }
486517 }
487518 </style >
488519 </body >
0 commit comments