mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
feat(splash): the pillar signature, and no more flash on refresh
Two separate problems, one of them a real bug. THE FLASH. The splash is server-rendered, so on a refresh the browser painted it and only then did hydration read sessionStorage, decide this visitor had already entered, and remove it. That one painted frame is the flash, and React cannot prevent it: even useLayoutEffect runs after the server HTML is painted. So the decision moved before paint. A tiny synchronous script in <head> reads the session flag and prefers-reduced-motion and stamps the root element; a plain CSS rule hides the overlay in the frame it would otherwise appear in. The overlay is `fixed`, so hiding it shifts no layout. Verified with a Playwright run: after reload the root is already stamped at readyState "interactive", the overlay computes to display:none, and the splash is not visible in any of twelve sampled frames. THE SPLASH ITSELF. It is now only the pillar signature: four dots become the four pillar symbols -- commerce, crypto, games, education -- become the word SUCH, then the page takes over. One generated asset (morph_splash*.svg) whose CSS timeline is scaled to land exactly on 1337ms, from such-graphics 628fcc1. SPLASH_MS here and motion.splash.ms there are the same number by construction: a gate that closes early cuts the last letter mid-draw. Everything else on the splash is gone -- tagline, supporting line, the four corner nav links, the chevron, the water-refraction filter and its pre-warm. The corner links were unreachable inside a gate this short anyway, and the header carries them. splash-logo-data.ts goes with them, now orphaned. Perf, measured on a local production build: mobile 97, desktop 98, unchanged from what production scores today. CLS stays ~0. LCP moves 0.9s -> 1.2s on mobile, because the mark is now a 12KB request where the old logo was an inlined data URI. Both theme variants are fetched (3.7KB each gzipped) since `dark:hidden` hides but does not prevent the download, so half of that is waste. Fixable by inlining as data URIs the way splash-logo-data.ts did, at the cost of ~30KB in the bundle; not done here because the score did not move.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<defs><filter id="sg-sym-glow"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>
|
||||
<style>
|
||||
.d { transform-box: fill-box; transform-origin: center; transform: scale(0); animation: sg-dot-in 149ms ease-in-out forwards, sg-dot-out 149ms ease-in-out forwards; }
|
||||
@keyframes sg-dot-in { to { transform: scale(1); } }
|
||||
@keyframes sg-dot-out { from { transform: scale(1); opacity: 1; } to { transform: scale(0.55); opacity: 0; } }
|
||||
.sym { transform-box: fill-box; transform-origin: center; opacity: 0; transform: scale(0.62); animation: sg-sym-in 149ms ease-in-out forwards, sg-sym-out 318ms ease-in-out forwards; }
|
||||
@keyframes sg-sym-in { to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes sg-sym-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.55); } }
|
||||
.g { stroke-dasharray: 400; stroke-dashoffset: 400; animation: sg-draw 566ms ease-in-out forwards; }
|
||||
@keyframes sg-draw { to { stroke-dashoffset: 0; } }
|
||||
.frame { opacity: 0; animation: sg-fade 318ms ease-out forwards; }
|
||||
@keyframes sg-fade { to { opacity: var(--frame-opacity, 0.45); } }
|
||||
.d-s { animation-delay: 0ms, 191ms; }
|
||||
.sym-s { animation-delay: 191ms, 580ms; }
|
||||
.g-s { animation-delay: 580ms; }
|
||||
.d-u { animation-delay: 64ms, 255ms; }
|
||||
.sym-u { animation-delay: 255ms, 644ms; }
|
||||
.g-u { animation-delay: 644ms; }
|
||||
.d-c { animation-delay: 127ms, 318ms; }
|
||||
.sym-c { animation-delay: 318ms, 707ms; }
|
||||
.g-c { animation-delay: 707ms; }
|
||||
.d-h { animation-delay: 191ms, 382ms; }
|
||||
.sym-h { animation-delay: 382ms, 771ms; }
|
||||
.g-h { animation-delay: 771ms; }
|
||||
.frame { animation-delay: 580ms; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.d, .sym { display: none; }
|
||||
.g { animation: none; stroke-dashoffset: 0; }
|
||||
.frame { animation: none; opacity: 0.45; }
|
||||
}
|
||||
</style>
|
||||
<rect width="200" height="200" rx="26" fill="#0a0a0a"/>
|
||||
<rect class="frame" x="14" y="14" width="172" height="172" rx="12" fill="none" stroke="#45c99e" stroke-width="3.5" opacity="0.7"/>
|
||||
<g transform="translate(60,59.25)"><g class="d d-s"><circle r="24" fill="#35925c"/></g></g>
|
||||
<g transform="translate(38,39.71) scale(0.4247)"><g class="sym sym-s"><path d="M4.3 -0.10000000000000853H12.5Q15.5 -0.20000000000000284 18.0 1.3999999999999986Q20.5 3.0 21.700000000000003 5.599999999999994H95.80000000000001Q99.2 5.599999999999994 101.4 8.399999999999999Q103.60000000000001 11.200000000000003 102.7 14.700000000000003L95.30000000000001 42.199999999999996Q94.2 46.6 90.7 49.2Q87.2 51.8 82.80000000000001 51.8H30.8L31.700000000000003 56.9Q32.0 58.4 33.2 59.4Q34.4 60.4 36.0 60.5H88.0Q89.80000000000001 60.599999999999994 91.0 61.8Q92.2 63.0 92.30000000000001 64.8Q92.2 66.6 91.0 67.8Q89.80000000000001 69.0 88.0 69.1H36.0Q31.400000000000002 69.2 27.800000000000004 66.2Q24.200000000000003 63.2 23.3 58.599999999999994L14.0 9.700000000000003Q13.8 8.5 12.5 8.5H4.3Q2.4000000000000004 8.399999999999991 1.2000000000000002 7.199999999999996Q0.0 6.0 0.0 4.199999999999996Q0.0 2.3999999999999915 1.2000000000000002 1.1999999999999957Q2.4000000000000004 0.0 4.3 -0.10000000000000853ZM23.1 83.5Q23.1 79.9 25.6 77.4Q28.1 74.9 31.700000000000003 74.9Q35.2 74.9 37.800000000000004 77.4Q40.400000000000006 79.9 40.400000000000006 83.5Q40.400000000000006 87.0 37.800000000000004 89.6Q35.2 92.2 31.700000000000003 92.2Q28.1 92.2 25.6 89.6Q23.1 87.0 23.1 83.5ZM83.7 74.9Q87.30000000000001 74.9 89.80000000000001 77.4Q92.30000000000001 79.9 92.30000000000001 83.5Q92.30000000000001 87.0 89.80000000000001 89.6Q87.30000000000001 92.2 83.7 92.2Q80.2 92.2 77.6 89.6Q75.0 87.0 75.0 83.5Q75.0 79.9 77.6 77.4Q80.2 74.9 83.7 74.9Z" fill="#35925c" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<g transform="translate(137,59.25)"><g class="d d-u"><circle r="24" fill="#e8689e"/></g></g>
|
||||
<g transform="translate(120.6,37.35) scale(0.4762)"><g class="sym sym-u"><path d="M55.2 43.599999999999994Q69.0 46.8 67.7 61.599999999999994Q66.9 71.2 60.5 75.2Q54.1 79.2 42.0 79.8V92.2H33.2V80.2Q28.700000000000003 80.2 26.3 80.0V92.2H17.5V79.8H0.0L1.7000000000000002 69.4Q8.4 69.6 9.5 69.2Q11.0 68.6 11.3 66.6V27.4Q11.100000000000001 25.799999999999997 10.3 24.999999999999996Q9.5 24.199999999999996 8.3 23.799999999999997Q7.1000000000000005 23.4 5.300000000000001 23.4Q3.5 23.4 1.4000000000000001 23.4Q0.5 23.4 0.0 23.4V14.200000000000003H17.5V-0.20000000000000284H26.3V13.799999999999997Q28.6 13.799999999999997 33.2 13.799999999999997V-0.20000000000000284H42.0V14.200000000000003Q51.2 15.0 56.6 18.4Q62.0 21.799999999999997 62.7 29.0Q63.6 39.199999999999996 55.2 43.599999999999994ZM26.5 24.0V41.599999999999994Q26.700000000000003 41.599999999999994 28.5 41.599999999999994Q30.3 41.599999999999994 31.1 41.599999999999994Q32.0 41.599999999999994 33.900000000000006 41.5Q35.800000000000004 41.4 37.0 41.2Q38.2 41.0 39.900000000000006 40.599999999999994Q41.7 40.199999999999996 42.7 39.5Q43.7 38.8 44.7 38.0Q45.7 37.199999999999996 46.300000000000004 35.8Q46.800000000000004 34.4 46.800000000000004 32.8Q46.800000000000004 30.799999999999997 46.0 29.199999999999996Q45.1 27.599999999999994 43.5 26.599999999999994Q41.900000000000006 25.599999999999994 40.5 25.0Q39.1 24.4 36.5 24.199999999999996Q33.9 24.0 32.7 24.0Q31.5 24.0 29.1 24.0Q26.700000000000003 24.0 26.5 24.0ZM26.5 69.4Q26.900000000000002 69.6 28.900000000000002 69.6Q30.900000000000002 69.6 31.900000000000002 69.6Q33.1 69.6 35.300000000000004 69.4Q37.5 69.4 38.900000000000006 69.1Q40.300000000000004 68.8 42.400000000000006 68.4Q44.5 68.0 45.7 67.2Q46.900000000000006 66.4 48.1 65.4Q49.300000000000004 64.4 49.900000000000006 63.0Q50.5 61.599999999999994 50.5 59.8Q50.5 57.599999999999994 49.5 55.8Q48.5 54.0 46.7 53.0Q44.7 52.0 43.0 51.4Q41.300000000000004 50.8 38.300000000000004 50.599999999999994Q35.300000000000004 50.4 33.9 50.2Q32.5 50.2 29.6 50.2Q26.700000000000003 50.2 26.5 50.2Z" fill="#e8689e" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<g transform="translate(60,141)"><g class="d d-c"><circle r="24" fill="#4aade0"/></g></g>
|
||||
<g transform="translate(38,119) scale(0.4793)"><g class="sym sym-c"><path d="M70.9 12.599999999999994Q62.300000000000004 12.599999999999994 57.0 15.099999999999994Q51.7 12.599999999999994 45.85 12.599999999999994Q40.0 12.599999999999994 34.800000000000004 15.099999999999994Q29.5 12.599999999999994 20.900000000000002 12.599999999999994Q15.200000000000001 12.599999999999994 10.0 21.599999999999994Q5.7 28.699999999999996 2.7 39.699999999999996Q0.0 49.3 0.0 54.4Q0.0 56.9 3.7 59.05Q7.4 61.2 13.100000000000001 62.2Q15.200000000000001 69.2 23.700000000000003 74.0Q32.2 78.8 43.800000000000004 79.4V61.4Q37.6 57.5 37.5 54.4Q37.5 51.7 41.800000000000004 50.7Q43.800000000000004 50.099999999999994 45.900000000000006 50.099999999999994L48.0 50.3Q50.5 50.7 52.1 51.5Q54.2 52.5 54.300000000000004 54.4Q54.300000000000004 57.5 48.0 61.4V79.4Q59.5 78.8 68.05000000000001 74.0Q76.60000000000001 69.2 78.7 62.2Q84.4 61.2 88.10000000000001 59.05Q91.80000000000001 56.9 91.80000000000001 54.4Q91.80000000000001 49.3 89.10000000000001 39.699999999999996Q86.2 28.799999999999997 81.80000000000001 21.599999999999994Q76.60000000000001 12.599999999999994 70.9 12.599999999999994ZM13.3 53.8Q10.600000000000001 53.3 8.4 52.3Q9.0 46.8 11.3 39.4Q13.600000000000001 32.8 16.450000000000003 27.299999999999997Q19.3 21.799999999999997 21.1 21.0Q23.200000000000003 21.0 26.400000000000002 21.4H26.6Q20.900000000000002 27.799999999999997 17.0 38.4Q14.5 45.6 13.3 53.8ZM33.4 46.0Q31.6 46.0 30.450000000000003 44.849999999999994Q29.3 43.699999999999996 29.3 41.89999999999999Q29.3 40.099999999999994 30.450000000000003 38.849999999999994Q31.6 37.599999999999994 33.400000000000006 37.599999999999994Q35.2 37.599999999999994 36.35 38.849999999999994Q37.5 40.099999999999994 37.5 41.89999999999999Q37.5 43.699999999999996 36.35 44.849999999999994Q35.2 46.0 33.4 46.0ZM58.400000000000006 46.0Q56.6 46.0 55.45 44.849999999999994Q54.300000000000004 43.699999999999996 54.300000000000004 41.89999999999999Q54.300000000000004 40.099999999999994 55.45 38.849999999999994Q56.6 37.599999999999994 58.400000000000006 37.599999999999994Q60.2 37.599999999999994 61.35 38.849999999999994Q62.5 40.099999999999994 62.5 41.89999999999999Q62.5 43.699999999999996 61.35 44.849999999999994Q60.2 46.0 58.400000000000006 46.0ZM78.5 53.8Q77.30000000000001 45.6 74.8 38.4Q70.9 27.9 65.2 21.4H65.4Q68.5 21.0 70.7 21.0Q72.5 21.799999999999997 75.30000000000001 27.249999999999996Q78.10000000000001 32.699999999999996 80.5 39.4Q82.80000000000001 46.9 83.4 52.3Q81.4 53.3 78.5 53.8Z" fill="#4aade0" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<g transform="translate(137,141)"><g class="d d-h"><circle r="24" fill="#3ad4c6"/></g></g>
|
||||
<g transform="translate(115.7,113.6) scale(0.5962)"><g class="sym sym-h"><path d="M35.800000000000004 41.9Q37.6 41.9 38.75 43.05Q39.900000000000006 44.199999999999996 39.900000000000006 46.0Q39.900000000000006 47.8 38.75 48.949999999999996Q37.6 50.099999999999994 35.8 50.099999999999994Q34.0 50.099999999999994 32.85 48.949999999999996Q31.700000000000003 47.8 31.700000000000003 46.0Q31.700000000000003 44.199999999999996 32.85 43.05Q34.0 41.9 35.800000000000004 41.9ZM3.4000000000000004 13.599999999999994Q7.9 9.099999999999994 16.650000000000002 10.749999999999993Q25.400000000000002 12.399999999999991 35.800000000000004 19.199999999999996Q46.2 12.399999999999991 54.95 10.749999999999993Q63.7 9.099999999999994 68.2 13.599999999999994Q72.7 18.099999999999994 71.05000000000001 26.849999999999994Q69.4 35.599999999999994 62.6 46.0Q69.4 56.4 71.05000000000001 65.15Q72.7 73.9 68.2 78.4Q63.7 82.9 54.95 81.25Q46.2 79.6 35.800000000000004 72.8Q25.400000000000002 79.6 16.650000000000002 81.25Q7.9 82.9 3.4000000000000004 78.4Q-1.1 73.9 0.55 65.15Q2.2 56.4 9.0 46.0Q2.2 35.599999999999994 0.55 26.849999999999994Q-1.1 18.099999999999994 3.4000000000000004 13.599999999999994ZM50.6 31.199999999999996Q54.1 34.9 57.7 39.199999999999996Q62.0 32.4 63.25 27.099999999999998Q64.5 21.799999999999997 62.25 19.549999999999997Q60.0 17.299999999999997 54.75 18.549999999999997Q49.5 19.799999999999997 42.6 24.099999999999994Q46.900000000000006 27.699999999999996 50.6 31.199999999999996ZM21.0 60.8Q17.5 57.099999999999994 13.9 52.8Q9.600000000000001 59.599999999999994 8.350000000000001 64.9Q7.1000000000000005 70.2 9.350000000000001 72.45Q11.600000000000001 74.7 16.85 73.45Q22.1 72.2 29.0 67.9Q24.700000000000003 64.3 21.0 60.8ZM9.200000000000001 19.4Q7.1000000000000005 21.699999999999996 8.350000000000001 26.999999999999996Q9.600000000000001 32.3 13.9 39.199999999999996Q17.400000000000002 34.9 21.050000000000004 31.249999999999996Q24.700000000000003 27.599999999999994 29.0 24.099999999999994Q22.200000000000003 19.799999999999997 16.85 18.549999999999997Q11.5 17.299999999999997 9.200000000000001 19.4ZM27.0 54.8Q31.1 58.9 35.800000000000004 62.8Q40.5 58.9 44.6 54.8Q48.7 50.7 52.6 46.0Q48.7 41.3 44.6 37.199999999999996Q40.5 33.099999999999994 35.800000000000004 29.199999999999996Q31.1 33.099999999999994 27.0 37.199999999999996Q22.900000000000002 41.3 19.0 46.0Q22.900000000000002 50.7 27.0 54.8ZM62.400000000000006 72.6Q64.5 70.3 63.25 65.0Q62.0 59.7 57.7 52.8Q54.2 57.099999999999994 50.550000000000004 60.75Q46.900000000000006 64.4 42.6 67.9Q49.400000000000006 72.2 54.75 73.45Q60.1 74.7 62.400000000000006 72.6Z" fill="#3ad4c6" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<path class="g g-s" d="M 78,36 H 36 V 58 H 78 V 82 H 36" fill="none" stroke="#35925c" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(3,0)"/>
|
||||
<path class="g g-u" d="m 118,36 v 34 c 0,12.15 9.85,13 22,13 12.15,0 22,-3.1 22,-13 V 36" fill="none" stroke="#e8689e" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(-3,0)"/>
|
||||
<path class="g g-c" d="M 78,118 H 36 V 164 H 78" fill="none" stroke="#4aade0" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(3,0)"/>
|
||||
<path class="g g-h" d="M 118,118 V 164 M 162,118 V 164 M 118,141 H 162" fill="none" stroke="#3ad4c6" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(-3,0)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,47 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<defs><filter id="sg-sym-glow"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>
|
||||
<style>
|
||||
.d { transform-box: fill-box; transform-origin: center; transform: scale(0); animation: sg-dot-in 149ms ease-in-out forwards, sg-dot-out 149ms ease-in-out forwards; }
|
||||
@keyframes sg-dot-in { to { transform: scale(1); } }
|
||||
@keyframes sg-dot-out { from { transform: scale(1); opacity: 1; } to { transform: scale(0.55); opacity: 0; } }
|
||||
.sym { transform-box: fill-box; transform-origin: center; opacity: 0; transform: scale(0.62); animation: sg-sym-in 149ms ease-in-out forwards, sg-sym-out 318ms ease-in-out forwards; }
|
||||
@keyframes sg-sym-in { to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes sg-sym-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.55); } }
|
||||
.g { stroke-dasharray: 400; stroke-dashoffset: 400; animation: sg-draw 566ms ease-in-out forwards; }
|
||||
@keyframes sg-draw { to { stroke-dashoffset: 0; } }
|
||||
.frame { opacity: 0; animation: sg-fade 318ms ease-out forwards; }
|
||||
@keyframes sg-fade { to { opacity: var(--frame-opacity, 0.45); } }
|
||||
.d-s { animation-delay: 0ms, 191ms; }
|
||||
.sym-s { animation-delay: 191ms, 580ms; }
|
||||
.g-s { animation-delay: 580ms; }
|
||||
.d-u { animation-delay: 64ms, 255ms; }
|
||||
.sym-u { animation-delay: 255ms, 644ms; }
|
||||
.g-u { animation-delay: 644ms; }
|
||||
.d-c { animation-delay: 127ms, 318ms; }
|
||||
.sym-c { animation-delay: 318ms, 707ms; }
|
||||
.g-c { animation-delay: 707ms; }
|
||||
.d-h { animation-delay: 191ms, 382ms; }
|
||||
.sym-h { animation-delay: 382ms, 771ms; }
|
||||
.g-h { animation-delay: 771ms; }
|
||||
.frame { animation-delay: 580ms; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.d, .sym { display: none; }
|
||||
.g { animation: none; stroke-dashoffset: 0; }
|
||||
.frame { animation: none; opacity: 0.45; }
|
||||
}
|
||||
</style>
|
||||
<rect width="200" height="200" rx="26" fill="#f7f8fa"/>
|
||||
<rect class="frame" x="14" y="14" width="172" height="172" rx="12" fill="none" stroke="#1a5c38" stroke-width="3.5" opacity="0.45"/>
|
||||
<g transform="translate(60,59.25)"><g class="d d-s"><circle r="24" fill="#1a5c38"/></g></g>
|
||||
<g transform="translate(38,39.71) scale(0.4247)"><g class="sym sym-s"><path d="M4.3 -0.10000000000000853H12.5Q15.5 -0.20000000000000284 18.0 1.3999999999999986Q20.5 3.0 21.700000000000003 5.599999999999994H95.80000000000001Q99.2 5.599999999999994 101.4 8.399999999999999Q103.60000000000001 11.200000000000003 102.7 14.700000000000003L95.30000000000001 42.199999999999996Q94.2 46.6 90.7 49.2Q87.2 51.8 82.80000000000001 51.8H30.8L31.700000000000003 56.9Q32.0 58.4 33.2 59.4Q34.4 60.4 36.0 60.5H88.0Q89.80000000000001 60.599999999999994 91.0 61.8Q92.2 63.0 92.30000000000001 64.8Q92.2 66.6 91.0 67.8Q89.80000000000001 69.0 88.0 69.1H36.0Q31.400000000000002 69.2 27.800000000000004 66.2Q24.200000000000003 63.2 23.3 58.599999999999994L14.0 9.700000000000003Q13.8 8.5 12.5 8.5H4.3Q2.4000000000000004 8.399999999999991 1.2000000000000002 7.199999999999996Q0.0 6.0 0.0 4.199999999999996Q0.0 2.3999999999999915 1.2000000000000002 1.1999999999999957Q2.4000000000000004 0.0 4.3 -0.10000000000000853ZM23.1 83.5Q23.1 79.9 25.6 77.4Q28.1 74.9 31.700000000000003 74.9Q35.2 74.9 37.800000000000004 77.4Q40.400000000000006 79.9 40.400000000000006 83.5Q40.400000000000006 87.0 37.800000000000004 89.6Q35.2 92.2 31.700000000000003 92.2Q28.1 92.2 25.6 89.6Q23.1 87.0 23.1 83.5ZM83.7 74.9Q87.30000000000001 74.9 89.80000000000001 77.4Q92.30000000000001 79.9 92.30000000000001 83.5Q92.30000000000001 87.0 89.80000000000001 89.6Q87.30000000000001 92.2 83.7 92.2Q80.2 92.2 77.6 89.6Q75.0 87.0 75.0 83.5Q75.0 79.9 77.6 77.4Q80.2 74.9 83.7 74.9Z" fill="#1a5c38" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<g transform="translate(137,59.25)"><g class="d d-u"><circle r="24" fill="#b83670"/></g></g>
|
||||
<g transform="translate(120.6,37.35) scale(0.4762)"><g class="sym sym-u"><path d="M55.2 43.599999999999994Q69.0 46.8 67.7 61.599999999999994Q66.9 71.2 60.5 75.2Q54.1 79.2 42.0 79.8V92.2H33.2V80.2Q28.700000000000003 80.2 26.3 80.0V92.2H17.5V79.8H0.0L1.7000000000000002 69.4Q8.4 69.6 9.5 69.2Q11.0 68.6 11.3 66.6V27.4Q11.100000000000001 25.799999999999997 10.3 24.999999999999996Q9.5 24.199999999999996 8.3 23.799999999999997Q7.1000000000000005 23.4 5.300000000000001 23.4Q3.5 23.4 1.4000000000000001 23.4Q0.5 23.4 0.0 23.4V14.200000000000003H17.5V-0.20000000000000284H26.3V13.799999999999997Q28.6 13.799999999999997 33.2 13.799999999999997V-0.20000000000000284H42.0V14.200000000000003Q51.2 15.0 56.6 18.4Q62.0 21.799999999999997 62.7 29.0Q63.6 39.199999999999996 55.2 43.599999999999994ZM26.5 24.0V41.599999999999994Q26.700000000000003 41.599999999999994 28.5 41.599999999999994Q30.3 41.599999999999994 31.1 41.599999999999994Q32.0 41.599999999999994 33.900000000000006 41.5Q35.800000000000004 41.4 37.0 41.2Q38.2 41.0 39.900000000000006 40.599999999999994Q41.7 40.199999999999996 42.7 39.5Q43.7 38.8 44.7 38.0Q45.7 37.199999999999996 46.300000000000004 35.8Q46.800000000000004 34.4 46.800000000000004 32.8Q46.800000000000004 30.799999999999997 46.0 29.199999999999996Q45.1 27.599999999999994 43.5 26.599999999999994Q41.900000000000006 25.599999999999994 40.5 25.0Q39.1 24.4 36.5 24.199999999999996Q33.9 24.0 32.7 24.0Q31.5 24.0 29.1 24.0Q26.700000000000003 24.0 26.5 24.0ZM26.5 69.4Q26.900000000000002 69.6 28.900000000000002 69.6Q30.900000000000002 69.6 31.900000000000002 69.6Q33.1 69.6 35.300000000000004 69.4Q37.5 69.4 38.900000000000006 69.1Q40.300000000000004 68.8 42.400000000000006 68.4Q44.5 68.0 45.7 67.2Q46.900000000000006 66.4 48.1 65.4Q49.300000000000004 64.4 49.900000000000006 63.0Q50.5 61.599999999999994 50.5 59.8Q50.5 57.599999999999994 49.5 55.8Q48.5 54.0 46.7 53.0Q44.7 52.0 43.0 51.4Q41.300000000000004 50.8 38.300000000000004 50.599999999999994Q35.300000000000004 50.4 33.9 50.2Q32.5 50.2 29.6 50.2Q26.700000000000003 50.2 26.5 50.2Z" fill="#b83670" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<g transform="translate(60,141)"><g class="d d-c"><circle r="24" fill="#176a92"/></g></g>
|
||||
<g transform="translate(38,119) scale(0.4793)"><g class="sym sym-c"><path d="M70.9 12.599999999999994Q62.300000000000004 12.599999999999994 57.0 15.099999999999994Q51.7 12.599999999999994 45.85 12.599999999999994Q40.0 12.599999999999994 34.800000000000004 15.099999999999994Q29.5 12.599999999999994 20.900000000000002 12.599999999999994Q15.200000000000001 12.599999999999994 10.0 21.599999999999994Q5.7 28.699999999999996 2.7 39.699999999999996Q0.0 49.3 0.0 54.4Q0.0 56.9 3.7 59.05Q7.4 61.2 13.100000000000001 62.2Q15.200000000000001 69.2 23.700000000000003 74.0Q32.2 78.8 43.800000000000004 79.4V61.4Q37.6 57.5 37.5 54.4Q37.5 51.7 41.800000000000004 50.7Q43.800000000000004 50.099999999999994 45.900000000000006 50.099999999999994L48.0 50.3Q50.5 50.7 52.1 51.5Q54.2 52.5 54.300000000000004 54.4Q54.300000000000004 57.5 48.0 61.4V79.4Q59.5 78.8 68.05000000000001 74.0Q76.60000000000001 69.2 78.7 62.2Q84.4 61.2 88.10000000000001 59.05Q91.80000000000001 56.9 91.80000000000001 54.4Q91.80000000000001 49.3 89.10000000000001 39.699999999999996Q86.2 28.799999999999997 81.80000000000001 21.599999999999994Q76.60000000000001 12.599999999999994 70.9 12.599999999999994ZM13.3 53.8Q10.600000000000001 53.3 8.4 52.3Q9.0 46.8 11.3 39.4Q13.600000000000001 32.8 16.450000000000003 27.299999999999997Q19.3 21.799999999999997 21.1 21.0Q23.200000000000003 21.0 26.400000000000002 21.4H26.6Q20.900000000000002 27.799999999999997 17.0 38.4Q14.5 45.6 13.3 53.8ZM33.4 46.0Q31.6 46.0 30.450000000000003 44.849999999999994Q29.3 43.699999999999996 29.3 41.89999999999999Q29.3 40.099999999999994 30.450000000000003 38.849999999999994Q31.6 37.599999999999994 33.400000000000006 37.599999999999994Q35.2 37.599999999999994 36.35 38.849999999999994Q37.5 40.099999999999994 37.5 41.89999999999999Q37.5 43.699999999999996 36.35 44.849999999999994Q35.2 46.0 33.4 46.0ZM58.400000000000006 46.0Q56.6 46.0 55.45 44.849999999999994Q54.300000000000004 43.699999999999996 54.300000000000004 41.89999999999999Q54.300000000000004 40.099999999999994 55.45 38.849999999999994Q56.6 37.599999999999994 58.400000000000006 37.599999999999994Q60.2 37.599999999999994 61.35 38.849999999999994Q62.5 40.099999999999994 62.5 41.89999999999999Q62.5 43.699999999999996 61.35 44.849999999999994Q60.2 46.0 58.400000000000006 46.0ZM78.5 53.8Q77.30000000000001 45.6 74.8 38.4Q70.9 27.9 65.2 21.4H65.4Q68.5 21.0 70.7 21.0Q72.5 21.799999999999997 75.30000000000001 27.249999999999996Q78.10000000000001 32.699999999999996 80.5 39.4Q82.80000000000001 46.9 83.4 52.3Q81.4 53.3 78.5 53.8Z" fill="#176a92" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<g transform="translate(137,141)"><g class="d d-h"><circle r="24" fill="#12857e"/></g></g>
|
||||
<g transform="translate(115.7,113.6) scale(0.5962)"><g class="sym sym-h"><path d="M35.800000000000004 41.9Q37.6 41.9 38.75 43.05Q39.900000000000006 44.199999999999996 39.900000000000006 46.0Q39.900000000000006 47.8 38.75 48.949999999999996Q37.6 50.099999999999994 35.8 50.099999999999994Q34.0 50.099999999999994 32.85 48.949999999999996Q31.700000000000003 47.8 31.700000000000003 46.0Q31.700000000000003 44.199999999999996 32.85 43.05Q34.0 41.9 35.800000000000004 41.9ZM3.4000000000000004 13.599999999999994Q7.9 9.099999999999994 16.650000000000002 10.749999999999993Q25.400000000000002 12.399999999999991 35.800000000000004 19.199999999999996Q46.2 12.399999999999991 54.95 10.749999999999993Q63.7 9.099999999999994 68.2 13.599999999999994Q72.7 18.099999999999994 71.05000000000001 26.849999999999994Q69.4 35.599999999999994 62.6 46.0Q69.4 56.4 71.05000000000001 65.15Q72.7 73.9 68.2 78.4Q63.7 82.9 54.95 81.25Q46.2 79.6 35.800000000000004 72.8Q25.400000000000002 79.6 16.650000000000002 81.25Q7.9 82.9 3.4000000000000004 78.4Q-1.1 73.9 0.55 65.15Q2.2 56.4 9.0 46.0Q2.2 35.599999999999994 0.55 26.849999999999994Q-1.1 18.099999999999994 3.4000000000000004 13.599999999999994ZM50.6 31.199999999999996Q54.1 34.9 57.7 39.199999999999996Q62.0 32.4 63.25 27.099999999999998Q64.5 21.799999999999997 62.25 19.549999999999997Q60.0 17.299999999999997 54.75 18.549999999999997Q49.5 19.799999999999997 42.6 24.099999999999994Q46.900000000000006 27.699999999999996 50.6 31.199999999999996ZM21.0 60.8Q17.5 57.099999999999994 13.9 52.8Q9.600000000000001 59.599999999999994 8.350000000000001 64.9Q7.1000000000000005 70.2 9.350000000000001 72.45Q11.600000000000001 74.7 16.85 73.45Q22.1 72.2 29.0 67.9Q24.700000000000003 64.3 21.0 60.8ZM9.200000000000001 19.4Q7.1000000000000005 21.699999999999996 8.350000000000001 26.999999999999996Q9.600000000000001 32.3 13.9 39.199999999999996Q17.400000000000002 34.9 21.050000000000004 31.249999999999996Q24.700000000000003 27.599999999999994 29.0 24.099999999999994Q22.200000000000003 19.799999999999997 16.85 18.549999999999997Q11.5 17.299999999999997 9.200000000000001 19.4ZM27.0 54.8Q31.1 58.9 35.800000000000004 62.8Q40.5 58.9 44.6 54.8Q48.7 50.7 52.6 46.0Q48.7 41.3 44.6 37.199999999999996Q40.5 33.099999999999994 35.800000000000004 29.199999999999996Q31.1 33.099999999999994 27.0 37.199999999999996Q22.900000000000002 41.3 19.0 46.0Q22.900000000000002 50.7 27.0 54.8ZM62.400000000000006 72.6Q64.5 70.3 63.25 65.0Q62.0 59.7 57.7 52.8Q54.2 57.099999999999994 50.550000000000004 60.75Q46.900000000000006 64.4 42.6 67.9Q49.400000000000006 72.2 54.75 73.45Q60.1 74.7 62.400000000000006 72.6Z" fill="#12857e" filter="url(#sg-sym-glow)"/></g></g>
|
||||
<path class="g g-s" d="M 78,36 H 36 V 58 H 78 V 82 H 36" fill="none" stroke="#1a5c38" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(3,0)"/>
|
||||
<path class="g g-u" d="m 118,36 v 34 c 0,12.15 9.85,13 22,13 12.15,0 22,-3.1 22,-13 V 36" fill="none" stroke="#b83670" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(-3,0)"/>
|
||||
<path class="g g-c" d="M 78,118 H 36 V 164 H 78" fill="none" stroke="#176a92" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(3,0)"/>
|
||||
<path class="g g-h" d="M 118,118 V 164 M 162,118 V 164 M 118,141 H 162" fill="none" stroke="#12857e" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" transform="translate(-3,0)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -319,3 +319,11 @@
|
||||
/* Soft aura behind the hero title (decorative). Gentle breathing scale/opacity. */
|
||||
@keyframes hero-aura { 0%, 100% { opacity: 0.55; transform: translateX(-50%) scale(1); } 50% { opacity: 0.8; transform: translateX(-50%) scale(1.08); } }
|
||||
.hero-aura { animation: hero-aura 7s ease-in-out infinite; }
|
||||
/* Pre-paint splash suppression. The inline <head> script stamps this attribute when the
|
||||
visitor has already entered this session (or asked for reduced motion), so the
|
||||
server-rendered overlay is hidden in the very first painted frame instead of being
|
||||
removed a frame later by hydration -- which is what the refresh flash was. The overlay
|
||||
is `fixed`, so this removes nothing from the document flow and shifts no layout. */
|
||||
html[data-splash="skip"] #splash-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,28 @@ export default function RootLayout({
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteData) }}
|
||||
/>
|
||||
{/*
|
||||
The splash is server-rendered, so on a refresh the browser painted it and only
|
||||
THEN did hydration decide this visitor had already entered and remove it. That
|
||||
one painted frame is the flash. React cannot prevent it: even useLayoutEffect
|
||||
runs after the first paint of the server HTML.
|
||||
|
||||
So the decision moves before paint. This script is synchronous and in <head>,
|
||||
which means it runs before the body is painted; it stamps the root element and
|
||||
a plain CSS rule (globals.css, [data-splash="skip"]) hides the overlay in the
|
||||
same frame it would otherwise have appeared in. The splash is `fixed`, so
|
||||
hiding it moves nothing and costs no layout shift.
|
||||
|
||||
Kept deliberately tiny and dependency-free because it is render-blocking.
|
||||
*/}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
"try{if(sessionStorage.getItem('such:entered')==='1'||" +
|
||||
"matchMedia('(prefers-reduced-motion: reduce)').matches)" +
|
||||
"{document.documentElement.dataset.splash='skip'}}catch(e){}",
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ChevronDown, LayoutGrid, Smartphone, Wrench, Info } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { SPLASH_LOGO_SRC_DARK, SPLASH_LOGO_SRC_LIGHT } from "./splash-logo-data";
|
||||
|
||||
const navButtons = [
|
||||
{ label: "Portfolio", href: "/products", Icon: LayoutGrid, corner: "left-4 top-6 sm:left-6 sm:top-24 lg:left-8 lg:top-32" },
|
||||
{ label: "Apps", href: "/apps", Icon: Smartphone, corner: "right-4 top-6 sm:right-6 sm:top-24 lg:right-8 lg:top-32" },
|
||||
{ label: "Services", href: "/services", Icon: Wrench, corner: "left-4 bottom-6 sm:left-6 sm:bottom-24 lg:left-8 lg:bottom-32" },
|
||||
{ label: "About", href: "/about", Icon: Info, corner: "right-4 bottom-6 sm:right-6 sm:bottom-24 lg:right-8 lg:bottom-32" },
|
||||
];
|
||||
// How long the splash holds before it hands over. This MUST match the pack's
|
||||
// motion.splash.ms: the morph asset is generated to end exactly on this number, so a gate
|
||||
// that closes early cuts the last letter mid-draw and one that closes late holds a still
|
||||
// frame. See such-graphics packs/such-software/pack.json.
|
||||
const SPLASH_MS = 1337;
|
||||
|
||||
// How long the splash holds before it enters the site on its own.
|
||||
const SPLASH_AUTO_ENTER_MS = 1500;
|
||||
// Long enough to read as a cross-fade, short enough that it does not feel like a second
|
||||
// gate stacked on the first.
|
||||
const FADE_MS = 260;
|
||||
|
||||
// The "SUCH" mark is served as /images/branding/splash-such.svg and rendered as an
|
||||
// <img> below: an <img> is a valid Largest-Contentful-Paint candidate, whereas an
|
||||
// inline <svg> is not, which is what was producing NO_LCP on mobile (where the
|
||||
// tagline text is hidden, leaving the logo as the only large element).
|
||||
|
||||
export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => void; sectionRef?: any; leaving?: boolean }) {
|
||||
const [entering, setEntering] = useState(false);
|
||||
/**
|
||||
* First-load splash: the pillar signature.
|
||||
*
|
||||
* Four dots become the four pillar symbols become the word SUCH, then the page takes over.
|
||||
* The whole thing is one generated SVG (morph_splash*.svg) whose CSS timeline is scaled to
|
||||
* land on SPLASH_MS, so this component only has to hold it and then get out of the way.
|
||||
*
|
||||
* It is a FIXED overlay from its first paint, never a block in document flow. That is not
|
||||
* cosmetic: in flow, removing it pulled a viewport of height out of the page and shifted
|
||||
* the hero by a full screen (CLS 1.0). A click used to hide that from CLS, since shifts
|
||||
* within 500ms of input are excused, but visitors who never clicked ate it in full.
|
||||
*
|
||||
* Returning visitors never see this at all, and crucially never see a FRAME of it: the
|
||||
* decision is made by a synchronous script in <head> (see layout.tsx) that hides the
|
||||
* overlay before the first paint. Doing it in React cannot work -- even useLayoutEffect
|
||||
* runs after the server HTML has been painted, which is the flash.
|
||||
*/
|
||||
export function HeroSplash({ onEnter, leaving }: { onEnter: () => void; leaving?: boolean }) {
|
||||
const [fadeOut, setFadeOut] = useState(false);
|
||||
// warmed: set on idle, well after LCP. It (a) paints a 1px element through the water
|
||||
// filter so the browser compiles the filter graph BEFORE the first real use (the
|
||||
// first-click stutter was the graph compiling mid-transition), and (b) turns on
|
||||
// will-change for the logo so its layer is promoted ahead of the zoom. Doing this
|
||||
// on requestIdleCallback keeps it invisible to Lighthouse.
|
||||
const [warmed, setWarmed] = useState(false);
|
||||
const dispRef = useRef<SVGFEDisplacementMapElement>(null);
|
||||
const turbRef = useRef<SVGFETurbulenceElement>(null);
|
||||
// Latest-handler ref so the window listeners below never hold a stale closure.
|
||||
const entered = useRef(false);
|
||||
// Latest-handler ref so the window listeners never hold a stale closure.
|
||||
const enterRef = useRef<() => void>(() => {});
|
||||
|
||||
// When the page asks the splash to leave, it first becomes a fixed overlay; one
|
||||
// frame later we drop opacity so it cross-fades into the revealed content.
|
||||
// Once asked to leave, drop opacity on the NEXT frame: the browser needs a painted
|
||||
// start value to transition from, and setting both in one frame is a cut, not a fade.
|
||||
useEffect(() => {
|
||||
if (!leaving) return;
|
||||
const id = requestAnimationFrame(() => setFadeOut(true));
|
||||
@@ -43,218 +44,63 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo
|
||||
}, [leaving]);
|
||||
|
||||
const handleEnter = () => {
|
||||
if (entering) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
onEnter();
|
||||
return;
|
||||
}
|
||||
setEntering(true);
|
||||
// Begin the cross-fade immediately so the water shimmer plays ON the splash as
|
||||
// it fades into the revealed content (snappier and more blended than shimmer-
|
||||
// then-cut).
|
||||
if (entered.current) return;
|
||||
entered.current = true;
|
||||
onEnter();
|
||||
// Water refraction: ramp the displacement scale up and back down while the
|
||||
// turbulence drifts, so the logo ripples like a disturbed liquid surface.
|
||||
const DURATION = 800;
|
||||
const MAX_SCALE = 40;
|
||||
const start = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min(1, (now - start) / DURATION);
|
||||
const env = Math.sin(Math.PI * t); // 0 -> 1 -> 0
|
||||
if (dispRef.current) dispRef.current.setAttribute("scale", String(env * MAX_SCALE));
|
||||
if (turbRef.current) {
|
||||
const f = 0.008 + t * 0.02;
|
||||
turbRef.current.setAttribute("baseFrequency", `${f.toFixed(4)} ${(f * 1.3).toFixed(4)}`);
|
||||
}
|
||||
if (t < 1) requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
enterRef.current = handleEnter;
|
||||
|
||||
// Scroll intent = enter. Scrolling PAST the splash already dismissed it (the shell's
|
||||
// IntersectionObserver), but only after a full screen of scrolling and with a hard
|
||||
// cut. First wheel tick / touch drag / scroll key now plays the SAME water-ripple
|
||||
// enter as a tap. Listeners are passive (never block the scroll) and the `entering`
|
||||
// guard inside handleEnter dedupes against the button path.
|
||||
// The gate: hand over when the morph ends.
|
||||
useEffect(() => {
|
||||
if (leaving) return;
|
||||
let touchY: number | null = null;
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
if (e.deltaY > 8) enterRef.current();
|
||||
};
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
touchY = e.touches[0]?.clientY ?? null;
|
||||
};
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (touchY == null) return;
|
||||
const y = e.touches[0]?.clientY ?? touchY;
|
||||
if (touchY - y > 24) { // a deliberate upward drag (scroll down), not a resting thumb
|
||||
touchY = null;
|
||||
enterRef.current();
|
||||
}
|
||||
};
|
||||
const id = window.setTimeout(() => enterRef.current(), SPLASH_MS);
|
||||
return () => window.clearTimeout(id);
|
||||
}, [leaving]);
|
||||
|
||||
// Any intent to move on skips the rest. Passive listeners so none of this can block a
|
||||
// scroll, and handleEnter dedupes against the timer.
|
||||
useEffect(() => {
|
||||
if (leaving) return;
|
||||
const skip = () => enterRef.current();
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowDown" || e.key === "PageDown" || e.key === " ") enterRef.current();
|
||||
if (["ArrowDown", "PageDown", " ", "Escape", "Enter"].includes(e.key)) skip();
|
||||
};
|
||||
window.addEventListener("wheel", onWheel, { passive: true });
|
||||
window.addEventListener("touchstart", onTouchStart, { passive: true });
|
||||
window.addEventListener("touchmove", onTouchMove, { passive: true });
|
||||
window.addEventListener("wheel", skip, { passive: true });
|
||||
window.addEventListener("touchstart", skip, { passive: true });
|
||||
window.addEventListener("pointerdown", skip, { passive: true });
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
window.removeEventListener("wheel", onWheel);
|
||||
window.removeEventListener("touchstart", onTouchStart);
|
||||
window.removeEventListener("touchmove", onTouchMove);
|
||||
window.removeEventListener("wheel", skip);
|
||||
window.removeEventListener("touchstart", skip);
|
||||
window.removeEventListener("pointerdown", skip);
|
||||
window.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [leaving]);
|
||||
|
||||
// Auto-advance: the splash is an intro, not a gate. After a beat it runs the same
|
||||
// water-ripple enter the visitor would have triggered themselves; a click, scroll,
|
||||
// or arrow key still fast-forwards it, and handleEnter's `entering` guard dedupes
|
||||
// the two paths. Reduced-motion visitors never get here (the shell skips the splash
|
||||
// outright), and the once-per-session flag is unchanged.
|
||||
useEffect(() => {
|
||||
if (leaving) return;
|
||||
const id = window.setTimeout(() => enterRef.current(), SPLASH_AUTO_ENTER_MS);
|
||||
return () => window.clearTimeout(id);
|
||||
}, [leaving]);
|
||||
|
||||
// Pre-warm on idle (post-LCP): compile the filter graph + promote the logo layer.
|
||||
useEffect(() => {
|
||||
const ric: typeof requestIdleCallback | undefined = (window as any).requestIdleCallback;
|
||||
const id = ric ? ric(() => setWarmed(true)) : window.setTimeout(() => setWarmed(true), 1500);
|
||||
return () => {
|
||||
const cic: typeof cancelIdleCallback | undefined = (window as any).cancelIdleCallback;
|
||||
if (ric && cic) cic(id as number);
|
||||
else window.clearTimeout(id as number);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// The splash is a FIXED overlay from its very first paint, never a block in document
|
||||
// flow. It used to be `relative min-h-[100svh]` and only became fixed on the way out,
|
||||
// which meant removing it pulled a whole viewport of height out of the flow and shoved
|
||||
// the hero up: a layout shift of almost exactly 1.0. A click hid that from CLS (shifts
|
||||
// within 500ms of input are excused), but reduced-motion and returning-session
|
||||
// visitors, who never click, ate it in full and scored 74. As an overlay it is out of
|
||||
// flow already, so dismissing it cannot reflow anything, and the auto-advance costs no
|
||||
// CLS either.
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
aria-label="Such Software"
|
||||
className={`fixed inset-0 z-50 flex flex-col items-center justify-center overflow-hidden bg-background px-4 py-12 text-center ${
|
||||
leaving
|
||||
? `pointer-events-none transition-opacity duration-700 ${fadeOut ? "opacity-0" : "opacity-100"}`
|
||||
: ""
|
||||
<div
|
||||
id="splash-overlay"
|
||||
role="presentation"
|
||||
className={`fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-background ${
|
||||
leaving ? `pointer-events-none transition-opacity ${fadeOut ? "opacity-0" : "opacity-100"}` : ""
|
||||
}`}
|
||||
style={leaving ? { transitionDuration: `${FADE_MS}ms` } : undefined}
|
||||
>
|
||||
{/* Click-to-enter: soft Cherenkov wash. The ripple itself is the SVG water
|
||||
filter refracting the logo (driven by handleEnter's rAF loop). */}
|
||||
{entering && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="splash-wash pointer-events-none fixed inset-0 z-20 backdrop-blur-sm"
|
||||
style={{ background: "radial-gradient(circle at center, rgba(34,211,238,0.28), transparent 65%)" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Water-refraction filter, animated by handleEnter */}
|
||||
<svg aria-hidden="true" focusable="false" className="pointer-events-none absolute h-0 w-0">
|
||||
<filter id="splash-water" x="-25%" y="-25%" width="150%" height="150%">
|
||||
<feTurbulence ref={turbRef} type="fractalNoise" baseFrequency="0.008 0.0104" numOctaves={2} seed={7} result="noise" />
|
||||
<feDisplacementMap ref={dispRef} in="SourceGraphic" in2="noise" scale={0} xChannelSelector="R" yChannelSelector="G" />
|
||||
</filter>
|
||||
</svg>
|
||||
|
||||
{/* Filter pre-warm: a near-invisible 1px paint through the filter (opacity must be
|
||||
non-zero or the paint is skipped and nothing compiles). Idle-gated via `warmed`. */}
|
||||
{warmed && !entering && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute bottom-0 left-0 h-px w-px opacity-[0.01]"
|
||||
style={{ filter: "url(#splash-water)" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Logo (click to enter) + pulsing Cherenkov glow */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEnter}
|
||||
aria-label="Enter Such Software"
|
||||
className="relative flex cursor-pointer items-center justify-center rounded-3xl border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute h-[120%] w-[120%] rounded-full bg-[radial-gradient(circle,rgba(34,211,238,0.2),transparent_65%)] motion-safe:animate-pulse"
|
||||
style={{ animationDuration: "4.5s" }}
|
||||
/>
|
||||
{/* Plain div, no framer: keeps the logo (the LCP element) a clean,
|
||||
statically-painted image. The water filter + zoom only apply on enter. */}
|
||||
<div
|
||||
className={entering ? "splash-zoom" : undefined}
|
||||
style={{
|
||||
filter: entering ? "url(#splash-water)" : undefined,
|
||||
willChange: warmed && !entering ? "transform, filter, opacity" : undefined,
|
||||
}}
|
||||
>
|
||||
{/* Data URI (no network request) so the logo paints with FCP; it is the
|
||||
mobile LCP element. */}
|
||||
<img
|
||||
src={SPLASH_LOGO_SRC_LIGHT}
|
||||
alt="Such Software"
|
||||
fetchPriority="high"
|
||||
className="h-auto w-[min(72vw,42vh)] dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src={SPLASH_LOGO_SRC_DARK}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
fetchPriority="high"
|
||||
className="hidden h-auto w-[min(72vw,42vh)] dark:block"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Cover text hidden on mobile (logo + corner icons carry it there); fades out on enter */}
|
||||
<p
|
||||
className={`mt-10 hidden sm:block brand-gradient-text pb-1 text-3xl font-bold text-transparent sm:text-4xl md:text-5xl transition-opacity duration-700 motion-safe:animate-in motion-safe:fade-in ${entering ? "opacity-0" : "opacity-100"}`}
|
||||
style={{ animationDuration: "700ms", animationDelay: "120ms", animationFillMode: "backwards" }}
|
||||
>
|
||||
Precision Engineering for Everyone
|
||||
</p>
|
||||
<p
|
||||
className={`mt-4 hidden max-w-xl text-base text-muted-foreground sm:block sm:text-lg transition-opacity duration-700 motion-safe:animate-in motion-safe:fade-in ${entering ? "opacity-0" : "opacity-100"}`}
|
||||
style={{ animationDuration: "700ms", animationDelay: "240ms", animationFillMode: "backwards" }}
|
||||
>
|
||||
A software studio in Kennett Square, PA. Custom e-commerce, apps, games,
|
||||
payments, and consulting.
|
||||
</p>
|
||||
|
||||
{/* Nav buttons in the four corners: icon-only on mobile, labeled pills on larger screens */}
|
||||
{navButtons.map((b, i) => (
|
||||
<Link
|
||||
key={b.href}
|
||||
href={b.href}
|
||||
aria-label={b.label}
|
||||
style={{ animationDuration: "700ms", animationDelay: `${360 + i * 110}ms`, animationFillMode: "backwards" }}
|
||||
className={`absolute flex items-center justify-center rounded-2xl border border-cyan-400/25 bg-foreground/[0.06] backdrop-blur-md shadow-md shadow-black/20 ring-1 ring-inset ring-white/10 transition-all motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-95 hover:scale-[1.03] hover:border-cyan-400/60 hover:bg-foreground/10 hover:text-cyan-600 dark:hover:text-cyan-400 hover:shadow-lg hover:shadow-cyan-500/15 h-12 w-12 sm:h-auto sm:w-36 sm:px-6 sm:py-4 lg:w-48 ${b.corner} ${entering ? "opacity-0 pointer-events-none" : ""}`}
|
||||
>
|
||||
<b.Icon className="h-5 w-5 sm:hidden" aria-hidden="true" />
|
||||
<span className="hidden text-base font-semibold sm:inline">{b.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Enter-the-site affordance */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEnter}
|
||||
style={{ animationDuration: "700ms", animationDelay: "700ms", animationFillMode: "backwards" }}
|
||||
className={`absolute bottom-6 left-1/2 flex -translate-x-1/2 flex-col items-center gap-1 text-xs uppercase tracking-[0.3em] text-muted-foreground transition-all motion-safe:animate-in motion-safe:fade-in hover:text-foreground focus-visible:text-foreground ${entering ? "opacity-0 pointer-events-none" : ""}`}
|
||||
>
|
||||
<span>Explore</span>
|
||||
<ChevronDown className="h-5 w-5 motion-safe:animate-bounce" aria-hidden="true" />
|
||||
</button>
|
||||
</section>
|
||||
{/* The mark is the only content, so it is the LCP candidate: an <img> is a valid
|
||||
LCP element where an inline <svg> is not. */}
|
||||
<img
|
||||
src="/images/animations/morph_splash_light.svg"
|
||||
alt="Such Software"
|
||||
fetchPriority="high"
|
||||
className="h-auto w-[min(62vw,38vh)] dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/images/animations/morph_splash.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
fetchPriority="high"
|
||||
className="hidden h-auto w-[min(62vw,38vh)] dark:block"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export function HomeShell({ children }: { children: ReactNode }) {
|
||||
// cross-fade overlay (button path only).
|
||||
const [splashUp, setSplashUp] = useState(true);
|
||||
const [leaving, setLeaving] = useState(false);
|
||||
const splashRef = useRef<HTMLElement>(null);
|
||||
const restoreScroll = useRef(0);
|
||||
const [nebulaPosition, setNebulaPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
@@ -141,7 +140,7 @@ export function HomeShell({ children }: { children: ReactNode }) {
|
||||
setTimeout(() => {
|
||||
setSplashUp(false);
|
||||
tryRelease();
|
||||
}, 800);
|
||||
}, 300); // the overlay's 260ms cross-fade, plus a frame
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -149,7 +148,7 @@ export function HomeShell({ children }: { children: ReactNode }) {
|
||||
<DynamicBackground nebulaPosition={nebulaPosition} visible={entered} />
|
||||
<Header floating visible={entered} />
|
||||
|
||||
{splashUp && <HeroSplash sectionRef={splashRef} onEnter={enterFromButton} leaving={leaving} />}
|
||||
{splashUp && <HeroSplash onEnter={enterFromButton} leaving={leaving} />}
|
||||
|
||||
{children}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// AUTO-GENERATED from packs/such-software/pack.json (such-graphics).
|
||||
// Inlined as data URIs so the splash logo (the homepage LCP element) needs no
|
||||
// network request and paints with first contentful paint. Two theme variants:
|
||||
// the pack's dark hues are the light hues lifted for contrast on near-black,
|
||||
// so each ground gets the palette that was tuned for it. Regenerate from the
|
||||
// pack if the mark changes; public/images/branding/splash-such.svg mirrors
|
||||
// the dark variant for anything that hotlinks it.
|
||||
export const SPLASH_LOGO_SRC_DARK =
|
||||
"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20200%20200%22%20role%3D%22img%22%20aria-label%3D%22Such%20Software%22%3E%3Cdefs%3E%3Cfilter%20id%3D%22splash-glow%22%20x%3D%22-30%25%22%20y%3D%22-30%25%22%20width%3D%22160%25%22%20height%3D%22160%25%22%3E%3CfeGaussianBlur%20stdDeviation%3D%221.6%22%20result%3D%22b%22%2F%3E%3CfeMerge%3E%3CfeMergeNode%20in%3D%22b%22%2F%3E%3CfeMergeNode%20in%3D%22SourceGraphic%22%2F%3E%3C%2FfeMerge%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3Crect%20x%3D%2214%22%20y%3D%2214%22%20width%3D%22172%22%20height%3D%22172%22%20rx%3D%2212%22%20fill%3D%22none%22%20stroke%3D%22%2345c99e%22%20stroke-width%3D%223.5%22%20filter%3D%22url%28%23splash-glow%29%22%20opacity%3D%220.45%22%2F%3E%3Cg%20fill%3D%22none%22%20stroke-width%3D%2214%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M%2078%2C36%20H%2036%20V%2058%20H%2078%20V%2082%20H%2036%22%20stroke%3D%22%2335925c%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22m%20118%2C36%20v%2034%20c%200%2C12.15%209.85%2C13%2022%2C13%2012.15%2C0%2022%2C-3.1%2022%2C-13%20V%2036%22%20stroke%3D%22%23e8689e%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%2078%2C118%20H%2036%20V%20164%20H%2078%22%20stroke%3D%22%234aade0%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%20118%2C118%20V%20164%20M%20162%2C118%20V%20164%20M%20118%2C141%20H%20162%22%20stroke%3D%22%233ad4c6%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E";
|
||||
export const SPLASH_LOGO_SRC_LIGHT =
|
||||
"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20200%20200%22%20role%3D%22img%22%20aria-label%3D%22Such%20Software%22%3E%3Cdefs%3E%3Cfilter%20id%3D%22splash-glow%22%20x%3D%22-30%25%22%20y%3D%22-30%25%22%20width%3D%22160%25%22%20height%3D%22160%25%22%3E%3CfeGaussianBlur%20stdDeviation%3D%221.6%22%20result%3D%22b%22%2F%3E%3CfeMerge%3E%3CfeMergeNode%20in%3D%22b%22%2F%3E%3CfeMergeNode%20in%3D%22SourceGraphic%22%2F%3E%3C%2FfeMerge%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3Crect%20x%3D%2214%22%20y%3D%2214%22%20width%3D%22172%22%20height%3D%22172%22%20rx%3D%2212%22%20fill%3D%22none%22%20stroke%3D%22%231a5c38%22%20stroke-width%3D%223.5%22%20filter%3D%22url%28%23splash-glow%29%22%20opacity%3D%220.45%22%2F%3E%3Cg%20fill%3D%22none%22%20stroke-width%3D%2214%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M%2078%2C36%20H%2036%20V%2058%20H%2078%20V%2082%20H%2036%22%20stroke%3D%22%231a5c38%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22m%20118%2C36%20v%2034%20c%200%2C12.15%209.85%2C13%2022%2C13%2012.15%2C0%2022%2C-3.1%2022%2C-13%20V%2036%22%20stroke%3D%22%23b83670%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%2078%2C118%20H%2036%20V%20164%20H%2078%22%20stroke%3D%22%23176a92%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%20118%2C118%20V%20164%20M%20162%2C118%20V%20164%20M%20118%2C141%20H%20162%22%20stroke%3D%22%2312857e%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E";
|
||||
Reference in New Issue
Block a user