From 9dfe15d242a2511cda8818eec44296064de57cb5 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 2 Apr 2026 19:37:39 -0400 Subject: [PATCH 1/4] kinda working --- src/app/App.module.css | 10 +++- src/app/components/EventCard.module.css | 30 +++++++---- src/app/components/EventRail.module.css | 38 +++++++++----- src/app/components/OrgCard.module.css | 30 +++++++---- src/app/components/OrgRail.module.css | 38 +++++++++----- src/app/sections/EventsSection.module.css | 27 ++++++---- src/app/sections/HomeSection.module.css | 9 ++-- src/app/sections/OrgsSection.module.css | 25 +++++---- src/app/sections/ProfileSection.module.css | 34 +++++++++--- src/lander/Lander.module.css | 11 +++- .../sections/CapyRailSection.module.css | 48 ++++++++++++----- src/lander/sections/ContactSection.module.css | 31 +++++++---- .../sections/FeaturesSection.module.css | 10 +++- src/lander/sections/HeroSection.module.css | 52 ++++++++++++++----- .../sections/InterfaceSection.module.css | 29 ++++++++--- src/shared/components/GlassCard.module.css | 9 ++-- 16 files changed, 303 insertions(+), 128 deletions(-) diff --git a/src/app/App.module.css b/src/app/App.module.css index 689bae7..9c539f6 100644 --- a/src/app/App.module.css +++ b/src/app/App.module.css @@ -1,4 +1,7 @@ .appRoot { + --app-track-padding-top: clamp(72px, 14svh, 121px); + --app-track-padding-bottom: clamp(16px, 3svh, var(--space-24)); + --app-panel-height: calc(100svh - var(--app-track-padding-top) - var(--app-track-padding-bottom)); width: 100vw; min-height: 100svh; overflow: hidden; @@ -36,7 +39,7 @@ .horizontalScroller { width: 100vw; - height: 100vh; + height: 100svh; overflow-x: auto; overflow-y: hidden; display: flex; @@ -58,12 +61,15 @@ width: max-content; height: 100%; min-height: 100%; - padding: 121px var(--space-24) var(--space-24) var(--space-24); + padding: var(--app-track-padding-top) var(--space-24) var(--app-track-padding-bottom) + var(--space-24); } /* Ensure sections match the panel styling */ .horizontalScroller :global(.panel) { width: calc(100vw - calc(var(--space-24) * 2)); + height: var(--app-panel-height); + min-height: var(--app-panel-height); flex-shrink: 0; scroll-snap-align: center; border-radius: var(--radius-card); diff --git a/src/app/components/EventCard.module.css b/src/app/components/EventCard.module.css index 710b789..a11e6a3 100644 --- a/src/app/components/EventCard.module.css +++ b/src/app/components/EventCard.module.css @@ -1,10 +1,10 @@ .eventCard { display: grid; - gap: var(--space-16); + gap: clamp(10px, 1.8svh, var(--space-16)); align-content: start; width: 100%; min-height: 0; - padding: 20px; + padding: clamp(16px, 2.4svh, 20px); appearance: none; border-radius: 28px; border: 1px solid rgba(126, 225, 218, 0.28); @@ -35,38 +35,38 @@ .cardHeader { display: grid; - gap: var(--space-8); + gap: clamp(6px, 1svh, var(--space-8)); } .cardTitle { margin: 0; color: var(--c-text-light); - font: 600 24px/1.06 var(--font-display); + font: 600 clamp(20px, 3svh, 24px) / 1.06 var(--font-display); text-wrap: balance; } .cardDescription { margin: 0; color: rgba(249, 250, 250, 0.82); - font: 400 15px/1.35 var(--font-body); + font: 400 clamp(13px, 1.8svh, 15px) / 1.35 var(--font-body); } .metaList { display: grid; - gap: var(--space-8); + gap: clamp(6px, 1svh, var(--space-8)); margin: 0; } .metaRow { display: grid; - grid-template-columns: 58px minmax(0, 1fr); - gap: var(--space-16); + grid-template-columns: clamp(46px, 6svh, 58px) minmax(0, 1fr); + gap: clamp(10px, 1.6svh, var(--space-16)); align-items: start; } .metaRow dt { color: rgba(249, 250, 250, 0.54); - font: 600 12px/1.2 var(--font-body); + font: 600 clamp(11px, 1.5svh, 12px) / 1.2 var(--font-body); text-transform: uppercase; letter-spacing: 0.08em; } @@ -74,7 +74,7 @@ .metaRow dd { margin: 0; color: var(--c-text-light); - font: 500 14px/1.3 var(--font-body); + font: 500 clamp(13px, 1.7svh, 14px) / 1.3 var(--font-body); } @media (max-width: 720px) { @@ -86,3 +86,13 @@ font-size: 22px; } } + +@media (max-height: 760px) { + .eventCard { + gap: 10px; + } + + .metaRow { + grid-template-columns: 46px minmax(0, 1fr); + } +} diff --git a/src/app/components/EventRail.module.css b/src/app/components/EventRail.module.css index 8d8346d..d2ca5c5 100644 --- a/src/app/components/EventRail.module.css +++ b/src/app/components/EventRail.module.css @@ -2,22 +2,22 @@ display: grid; grid-template-rows: auto auto; align-content: start; - gap: 14px; + gap: clamp(10px, 1.8svh, 14px); overflow: hidden; padding-bottom: 12px; - min-height: 390px; + min-height: clamp(280px, 48svh, 390px); } .groupHeader { display: grid; gap: 0; - padding-right: 64px; + padding-right: clamp(24px, 4vw, 64px); } .groupTitle { margin: 0; color: var(--c-text-light); - font: 600 28px/1 var(--font-display); + font: 600 clamp(24px, 3.8svh, 28px) / 1 var(--font-display); text-transform: lowercase; } @@ -26,28 +26,28 @@ grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--space-16); - padding-right: 64px; + padding-right: clamp(24px, 4vw, 64px); } .emptyState { display: grid; place-items: center; - min-height: 276px; - padding-right: 64px; + min-height: clamp(200px, 32svh, 276px); + padding-right: clamp(24px, 4vw, 64px); color: rgba(249, 250, 250, 0.8); - font: 500 20px/1.4 var(--font-body); + font: 500 clamp(18px, 2.8svh, 20px) / 1.4 var(--font-body); letter-spacing: 0.01em; text-align: center; } .controlButton { - width: 48px; - height: 48px; + width: clamp(42px, 6.2svh, 48px); + height: clamp(42px, 6.2svh, 48px); border: 1px solid rgba(249, 250, 250, 0.28); border-radius: 999px; background: rgba(255, 255, 255, 0.08); color: var(--c-text-light); - font: 700 22px/1 var(--font-display); + font: 700 clamp(20px, 3svh, 22px) / 1 var(--font-display); backdrop-filter: blur(var(--glass-blur-soft)); transition: background 180ms var(--ease-premium), @@ -75,7 +75,7 @@ --right-edge-fade: 72px; display: grid; grid-auto-flow: column; - grid-auto-columns: minmax(320px, 440px); + grid-auto-columns: clamp(260px, 32vw, 440px); gap: var(--space-24); overflow-x: auto; overflow-y: visible; @@ -157,3 +157,17 @@ font-size: 18px; } } + +@media (max-height: 760px) { + .groupHeader, + .railShell, + .emptyState { + padding-right: 24px; + } + + .carousel { + --left-edge-fade: 36px; + --right-edge-fade: 36px; + grid-auto-columns: clamp(240px, 30vw, 360px); + } +} diff --git a/src/app/components/OrgCard.module.css b/src/app/components/OrgCard.module.css index f645c6f..e21b356 100644 --- a/src/app/components/OrgCard.module.css +++ b/src/app/components/OrgCard.module.css @@ -1,10 +1,10 @@ .orgCard { display: grid; - gap: var(--space-16); + gap: clamp(10px, 1.8svh, var(--space-16)); align-content: start; width: 100%; min-height: 0; - padding: 20px; + padding: clamp(16px, 2.4svh, 20px); appearance: none; border-radius: 28px; border: 1px solid rgba(126, 225, 218, 0.28); @@ -35,38 +35,38 @@ .cardHeader { display: grid; - gap: var(--space-8); + gap: clamp(6px, 1svh, var(--space-8)); } .cardTitle { margin: 0; color: var(--c-text-light); - font: 600 24px/1.06 var(--font-display); + font: 600 clamp(20px, 3svh, 24px) / 1.06 var(--font-display); text-wrap: balance; } .cardDescription { margin: 0; color: rgba(249, 250, 250, 0.82); - font: 400 15px/1.35 var(--font-body); + font: 400 clamp(13px, 1.8svh, 15px) / 1.35 var(--font-body); } .metaList { display: grid; - gap: var(--space-8); + gap: clamp(6px, 1svh, var(--space-8)); margin: 0; } .metaRow { display: grid; - grid-template-columns: 58px minmax(0, 1fr); - gap: var(--space-16); + grid-template-columns: clamp(46px, 6svh, 58px) minmax(0, 1fr); + gap: clamp(10px, 1.6svh, var(--space-16)); align-items: start; } .metaRow dt { color: rgba(249, 250, 250, 0.54); - font: 600 12px/1.2 var(--font-body); + font: 600 clamp(11px, 1.5svh, 12px) / 1.2 var(--font-body); text-transform: uppercase; letter-spacing: 0.08em; } @@ -74,7 +74,7 @@ .metaRow dd { margin: 0; color: var(--c-text-light); - font: 500 14px/1.3 var(--font-body); + font: 500 clamp(13px, 1.7svh, 14px) / 1.3 var(--font-body); } @media (max-width: 720px) { @@ -86,3 +86,13 @@ font-size: 22px; } } + +@media (max-height: 760px) { + .orgCard { + gap: 10px; + } + + .metaRow { + grid-template-columns: 46px minmax(0, 1fr); + } +} diff --git a/src/app/components/OrgRail.module.css b/src/app/components/OrgRail.module.css index 3c2482a..feec3d4 100644 --- a/src/app/components/OrgRail.module.css +++ b/src/app/components/OrgRail.module.css @@ -2,22 +2,22 @@ display: grid; grid-template-rows: auto auto; align-content: start; - gap: 14px; + gap: clamp(10px, 1.8svh, 14px); overflow: hidden; padding-bottom: 12px; - min-height: 390px; + min-height: clamp(280px, 48svh, 390px); } .groupHeader { display: grid; gap: 0; - padding-right: 64px; + padding-right: clamp(24px, 4vw, 64px); } .groupTitle { margin: 0; color: var(--c-text-light); - font: 600 28px/1 var(--font-display); + font: 600 clamp(24px, 3.8svh, 28px) / 1 var(--font-display); text-transform: lowercase; } @@ -26,28 +26,28 @@ grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--space-16); - padding-right: 64px; + padding-right: clamp(24px, 4vw, 64px); } .emptyState { display: grid; place-items: center; - min-height: 276px; - padding-right: 64px; + min-height: clamp(200px, 32svh, 276px); + padding-right: clamp(24px, 4vw, 64px); color: rgba(249, 250, 250, 0.8); - font: 500 20px/1.4 var(--font-body); + font: 500 clamp(18px, 2.8svh, 20px) / 1.4 var(--font-body); letter-spacing: 0.01em; text-align: center; } .controlButton { - width: 48px; - height: 48px; + width: clamp(42px, 6.2svh, 48px); + height: clamp(42px, 6.2svh, 48px); border: 1px solid rgba(249, 250, 250, 0.28); border-radius: 999px; background: rgba(255, 255, 255, 0.08); color: var(--c-text-light); - font: 700 22px/1 var(--font-display); + font: 700 clamp(20px, 3svh, 22px) / 1 var(--font-display); backdrop-filter: blur(var(--glass-blur-soft)); transition: background 180ms var(--ease-premium), @@ -75,7 +75,7 @@ --right-edge-fade: 72px; display: grid; grid-auto-flow: column; - grid-auto-columns: minmax(320px, 440px); + grid-auto-columns: clamp(260px, 32vw, 440px); gap: var(--space-24); overflow-x: auto; overflow-y: visible; @@ -157,3 +157,17 @@ font-size: 18px; } } + +@media (max-height: 760px) { + .groupHeader, + .railShell, + .emptyState { + padding-right: 24px; + } + + .carousel { + --left-edge-fade: 36px; + --right-edge-fade: 36px; + grid-auto-columns: clamp(240px, 30vw, 360px); + } +} diff --git a/src/app/sections/EventsSection.module.css b/src/app/sections/EventsSection.module.css index d50b93f..4731468 100644 --- a/src/app/sections/EventsSection.module.css +++ b/src/app/sections/EventsSection.module.css @@ -3,8 +3,8 @@ display: grid; grid-template-rows: auto auto minmax(0, auto); align-content: start; - gap: 2rem; - padding: 5rem 0 6rem 5rem; + gap: clamp(1.25rem, 3svh, 2rem); + padding: clamp(3rem, 8svh, 5rem) 0 clamp(2rem, 9svh, 6rem) clamp(1.5rem, 5vw, 5rem); height: 100%; box-sizing: border-box; overflow-y: auto; @@ -18,8 +18,8 @@ .panelHeader { position: absolute; - top: 2.5rem; - right: 1.5rem; + top: clamp(1.25rem, 4svh, 2.5rem); + right: clamp(0.85rem, 2vw, 1.5rem); z-index: 1; } @@ -34,24 +34,24 @@ .title { margin: 0; color: var(--c-text-light); - font: 600 40px/0.96 var(--font-display); + font: 600 clamp(32px, 5.6svh, 40px) / 0.96 var(--font-display); text-transform: lowercase; } .createButton { - min-width: 56px; - width: 56px; + min-width: clamp(48px, 7svh, 56px); + width: clamp(48px, 7svh, 56px); padding: 0; flex: 0 0 auto; - font: 500 30px/1 var(--font-display); + font: 500 clamp(28px, 4svh, 30px) / 1 var(--font-display); } .status, .error { margin: 0; - padding-right: 4rem; + padding-right: clamp(1.5rem, 4vw, 4rem); color: rgba(249, 250, 250, 0.82); - font: 500 1rem/1.4 var(--font-body); + font: 500 clamp(0.95rem, 2svh, 1rem) / 1.4 var(--font-body); } .error { @@ -105,3 +105,10 @@ font-size: 28px; } } + +@media (max-height: 760px) { + .eventsPanel { + padding-top: clamp(2rem, 5svh, 3rem); + padding-bottom: 1.5rem; + } +} diff --git a/src/app/sections/HomeSection.module.css b/src/app/sections/HomeSection.module.css index 6919da0..b7b67ca 100644 --- a/src/app/sections/HomeSection.module.css +++ b/src/app/sections/HomeSection.module.css @@ -1,18 +1,19 @@ .homePanel { display: flex; flex-direction: column; - padding: 8rem 4rem; + justify-content: center; + padding: clamp(3rem, 12svh, 8rem) clamp(1.5rem, 5vw, 4rem); } .title { - font: 600 40px/1 var(--font-display); + font: 600 clamp(32px, 5.6svh, 40px) / 1 var(--font-display); text-transform: lowercase; - margin-bottom: 2rem; + margin-bottom: clamp(1rem, 3svh, 2rem); color: var(--c-text-light); } .description { max-width: 600px; - font: 400 18px/1.35 var(--font-body); + font: 400 clamp(16px, 2.2svh, 18px) / 1.35 var(--font-body); color: var(--c-text-light); } diff --git a/src/app/sections/OrgsSection.module.css b/src/app/sections/OrgsSection.module.css index 633c2bc..e2a31dd 100644 --- a/src/app/sections/OrgsSection.module.css +++ b/src/app/sections/OrgsSection.module.css @@ -3,8 +3,8 @@ display: grid; grid-template-rows: auto auto minmax(0, auto); align-content: start; - gap: 2rem; - padding: 5rem 0 6rem 5rem; + gap: clamp(1.25rem, 3svh, 2rem); + padding: clamp(3rem, 8svh, 5rem) 0 clamp(2rem, 9svh, 6rem) clamp(1.5rem, 5vw, 5rem); height: 100%; box-sizing: border-box; overflow-y: auto; @@ -18,25 +18,25 @@ .panelHeader { position: absolute; - top: 2.5rem; - right: 1.5rem; + top: clamp(1.25rem, 4svh, 2.5rem); + right: clamp(0.85rem, 2vw, 1.5rem); z-index: 1; } .createButton { - min-width: 56px; - width: 56px; + min-width: clamp(48px, 7svh, 56px); + width: clamp(48px, 7svh, 56px); padding: 0; flex: 0 0 auto; - font: 500 30px/1 var(--font-display); + font: 500 clamp(28px, 4svh, 30px) / 1 var(--font-display); } .status, .error { margin: 0; - padding-right: 4rem; + padding-right: clamp(1.5rem, 4vw, 4rem); color: rgba(249, 250, 250, 0.82); - font: 500 1rem/1.4 var(--font-body); + font: 500 clamp(0.95rem, 2svh, 1rem) / 1.4 var(--font-body); } .error { @@ -86,3 +86,10 @@ font-size: 28px; } } + +@media (max-height: 760px) { + .orgsPanel { + padding-top: clamp(2rem, 5svh, 3rem); + padding-bottom: 1.5rem; + } +} diff --git a/src/app/sections/ProfileSection.module.css b/src/app/sections/ProfileSection.module.css index 315a607..14292bc 100644 --- a/src/app/sections/ProfileSection.module.css +++ b/src/app/sections/ProfileSection.module.css @@ -2,7 +2,14 @@ display: flex; align-items: center; justify-content: center; - padding: 8rem 4rem; + padding: clamp(2rem, 8svh, 8rem) clamp(1.25rem, 4vw, 4rem); + overflow-y: auto; + overscroll-behavior-y: contain; + scrollbar-width: none; +} + +.profilePanel::-webkit-scrollbar { + display: none; } .shell { @@ -18,7 +25,7 @@ } .confirmText { - font: 400 18px/1.35 var(--font-body); + font: 400 clamp(16px, 2.1svh, 18px) / 1.35 var(--font-body); } .formStack { @@ -36,8 +43,8 @@ } .avatar { - width: 96px; - height: 96px; + width: clamp(80px, 12svh, 96px); + height: clamp(80px, 12svh, 96px); border-radius: 28px; display: grid; place-items: center; @@ -85,13 +92,13 @@ .fieldLabel { color: rgba(249, 250, 250, 0.66); - font: 600 0.92rem/1 var(--font-body); + font: 600 clamp(0.82rem, 1.8svh, 0.92rem) / 1 var(--font-body); text-transform: uppercase; letter-spacing: 0.08em; } .input { - min-height: 58px; + min-height: clamp(48px, 7.5svh, 58px); width: 100%; padding: 0 1rem; border-radius: 22px; @@ -144,7 +151,7 @@ .signInPrompt { display: grid; gap: 0.75rem; - padding: 3rem 2rem; + padding: clamp(1.75rem, 5svh, 3rem) clamp(1.25rem, 3vw, 2rem); border-radius: 28px; background: rgba(255, 255, 255, 0.04); border: 1px dashed rgba(180, 241, 235, 0.12); @@ -167,7 +174,7 @@ @media (max-width: 720px) { .profilePanel { - padding: 6rem 1.25rem 2rem; + padding: clamp(1.5rem, 4svh, 6rem) 1.25rem 2rem; } .profileHeader { @@ -185,3 +192,14 @@ flex-direction: column; } } + +@media (max-height: 760px) { + .shell, + .formStack { + gap: 1rem; + } + + .profileHeader { + margin-bottom: 0; + } +} diff --git a/src/lander/Lander.module.css b/src/lander/Lander.module.css index 7898279..99a9f8c 100644 --- a/src/lander/Lander.module.css +++ b/src/lander/Lander.module.css @@ -1,4 +1,9 @@ .appRoot { + --lander-track-padding-top: clamp(72px, 14svh, 121px); + --lander-track-padding-bottom: clamp(16px, 3svh, var(--space-24)); + --lander-panel-height: calc( + 100svh - var(--lander-track-padding-top) - var(--lander-track-padding-bottom) + ); min-height: 100svh; background: radial-gradient(circle at 10% -20%, rgba(94, 204, 196, 0.2), transparent 36%), @@ -57,10 +62,12 @@ width: max-content; height: 100%; min-height: 100%; - padding: 121px var(--space-24) var(--space-24) var(--space-24); + padding: var(--lander-track-padding-top) var(--space-24) var(--lander-track-padding-bottom) + var(--space-24); } :global(.panel) { - height: 100%; + height: var(--lander-panel-height); + min-height: var(--lander-panel-height); border-radius: var(--radius-card); } diff --git a/src/lander/sections/CapyRailSection.module.css b/src/lander/sections/CapyRailSection.module.css index a39518a..87e2597 100644 --- a/src/lander/sections/CapyRailSection.module.css +++ b/src/lander/sections/CapyRailSection.module.css @@ -2,7 +2,7 @@ position: relative; overflow: hidden; --rail-mark-vertical-inset: 24px; - width: 866px; + width: min(866px, calc(100vw - var(--space-24) * 2)); background: var(--c-surface-light); border: 1px solid var(--c-surface-border); color: var(--c-text-deep); @@ -12,22 +12,22 @@ .railContent { position: relative; z-index: 1; - width: 420px; - margin: 40px 0 40px 46px; - height: calc(100% - 80px); + width: min(420px, calc(100% - 92px)); + margin: clamp(24px, 4svh, 40px) 0 clamp(24px, 4svh, 40px) clamp(24px, 4vw, 46px); + height: calc(100% - clamp(48px, 8svh, 80px)); display: flex; flex-direction: column; } .railTop { margin: 0; - font: 400 24px/1.25 var(--font-body); + font: 400 clamp(20px, 3svh, 24px) / 1.25 var(--font-body); } .verticalMarkWrap { position: absolute; top: var(--rail-mark-vertical-inset); - left: 375px; + left: clamp(290px, 43%, 375px); transform: none; width: 178px; height: calc(100% - var(--rail-mark-vertical-inset) * 2); @@ -38,7 +38,7 @@ } .verticalMark { - width: 739px; + width: clamp(520px, 78svh, 739px); height: 178px; max-width: none; transform: rotate(-90deg) scale(1); @@ -47,20 +47,20 @@ .railLinks { display: grid; - gap: 62px; + gap: clamp(28px, 7svh, 62px); max-width: 380px; - margin-top: 132px; + margin-top: clamp(40px, 15svh, 132px); } .railLinkRow { display: grid; grid-template-columns: auto auto; - column-gap: 72px; + column-gap: clamp(28px, 6vw, 72px); } .railLinkBlock p { margin: 0; - font: 400 16px/1.5 var(--font-body); + font: 400 clamp(14px, 2svh, 16px) / 1.5 var(--font-body); } .columnTitle { @@ -70,7 +70,7 @@ .railSocial { display: grid; gap: 4px; - margin-bottom: 42px; + margin-bottom: clamp(24px, 5svh, 42px); } .railSocial p { @@ -93,7 +93,7 @@ .railStatus, .railFoot { margin: 0; - font: 400 16px/1.4 var(--font-body); + font: 400 clamp(14px, 2svh, 16px) / 1.4 var(--font-body); max-width: 420px; } @@ -106,3 +106,25 @@ .railMeta .railSocial { margin-top: 0; } + +@media (max-height: 760px) { + .capyRailPanel { + display: grid; + align-items: stretch; + } + + .railContent { + width: min(100% - 48px, 420px); + margin-right: 24px; + } + + .railLinks { + margin-top: 28px; + } + + .verticalMarkWrap { + left: auto; + right: -28px; + opacity: 0.28; + } +} diff --git a/src/lander/sections/ContactSection.module.css b/src/lander/sections/ContactSection.module.css index 495547d..16395f2 100644 --- a/src/lander/sections/ContactSection.module.css +++ b/src/lander/sections/ContactSection.module.css @@ -1,7 +1,7 @@ .contactPanel { - width: 1143px; + width: min(1143px, calc(100vw - var(--space-24) * 2)); border: 1px solid var(--c-surface-border); - padding: 232px 131px 78px; + padding: clamp(104px, 24svh, 232px) clamp(32px, 8vw, 131px) clamp(32px, 7svh, 78px); background: linear-gradient(180deg, rgba(57, 172, 163, 0.24), rgba(45, 133, 127, 0.16)); box-shadow: var(--glass-shadow); backdrop-filter: blur(var(--glass-blur)); @@ -12,7 +12,7 @@ font-family: var(--font-display); font-weight: 400; line-height: 1.05; - font-size: 96px; + font-size: clamp(48px, min(8vw, 14svh), 96px); color: var(--c-text-light); } @@ -30,13 +30,13 @@ .contactLead { margin-top: var(--space-24); color: var(--c-text-light); - font: 400 36px/1.3 var(--font-body); + font: 400 clamp(24px, 4.8svh, 36px) / 1.3 var(--font-body); } .emailPill { margin-top: 31px; - width: 288px; - min-height: 60px; + width: min(288px, 100%); + min-height: clamp(48px, 7svh, 60px); border-radius: var(--radius-pill); border: 1px solid rgba(154, 215, 210, 0.8); display: inline-flex; @@ -45,19 +45,28 @@ color: var(--c-bg-mid); text-decoration: none; background: linear-gradient(180deg, rgba(244, 248, 246, 0.95), rgba(218, 243, 240, 0.9)); - font: 400 24px/1 var(--font-body); + font: 400 clamp(18px, 3svh, 24px) / 1 var(--font-body); } .contactMeta { margin: 0 0 12px; - font: 400 16px/1.35 var(--font-body); + font: 400 clamp(14px, 2.1svh, 16px) / 1.35 var(--font-body); } @media (max-width: 1200px) { - .contactPanel h2 { - font-size: clamp(48px, 12vw, 72px); - } .contactLead { font-size: 26px; } } + +@media (max-height: 760px) { + .contactContent { + grid-template-rows: auto auto; + gap: 16px; + } + + .contactLead, + .emailPill { + margin-top: 18px; + } +} diff --git a/src/lander/sections/FeaturesSection.module.css b/src/lander/sections/FeaturesSection.module.css index e85e413..8eafafe 100644 --- a/src/lander/sections/FeaturesSection.module.css +++ b/src/lander/sections/FeaturesSection.module.css @@ -1,10 +1,12 @@ .featuresPanel { display: grid; - grid-template-columns: 525px 710px; + grid-template-columns: minmax(360px, 525px) minmax(440px, 710px); grid-template-rows: repeat(6, minmax(0, 1fr)); gap: var(--space-24); height: 100%; overflow: hidden; + width: min(1259px, calc(100vw - var(--space-24) * 2)); + min-width: 0; } .card-col1-row1 { @@ -31,3 +33,9 @@ grid-column: 2; grid-row: 5 / span 2; } + +@media (max-height: 820px) { + .featuresPanel { + gap: var(--space-16); + } +} diff --git a/src/lander/sections/HeroSection.module.css b/src/lander/sections/HeroSection.module.css index 5ee5ba8..7bf118a 100644 --- a/src/lander/sections/HeroSection.module.css +++ b/src/lander/sections/HeroSection.module.css @@ -1,13 +1,14 @@ .heroPanel { - width: 1415px; + width: min(1415px, calc(100vw - var(--space-24) * 2)); border: 1px solid var(--c-surface-border); background: linear-gradient(180deg, rgba(56, 171, 162, 0.2), rgba(43, 137, 130, 0.16)); backdrop-filter: blur(var(--glass-blur)); - padding: 210px 100px 60px; + padding: clamp(112px, 22svh, 210px) clamp(32px, 7vw, 100px) clamp(32px, 7svh, 60px); display: flex; flex-direction: column; align-items: flex-start; box-shadow: var(--glass-shadow); + overflow: hidden; } .heroRows { @@ -22,7 +23,7 @@ display: grid; gap: var(--space-8); font-family: var(--font-display); - font-size: 180px; + font-size: clamp(72px, min(14vw, 22svh), 180px); font-weight: 700; letter-spacing: 0.2px; line-height: 0.92; @@ -36,6 +37,7 @@ .heroDescription { display: flex; flex-direction: column; + gap: clamp(10px, 1.8svh, 16px); width: fit-content; max-width: 360px; border: 1px solid var(--c-surface-border); @@ -43,12 +45,12 @@ box-shadow: var(--glass-shadow); backdrop-filter: blur(var(--glass-blur)); border-radius: var(--radius-card); - padding: 36px; + padding: clamp(20px, 3.5svh, 36px); } .heroDescription p { margin: 0; - font: 400 18px/1.35 var(--font-body); + font: 400 clamp(16px, 1.9svh, 18px) / 1.35 var(--font-body); } .heroRowBottom { @@ -56,13 +58,15 @@ align-items: flex-start; justify-content: space-between; gap: 20px; + width: 100%; } .heroCtas { display: flex; + flex-wrap: wrap; gap: 18px; justify-content: flex-start; - margin: 30px 10px 0; + margin: clamp(20px, 3.5svh, 30px) 10px 0; } .howModalBackdrop { @@ -103,16 +107,40 @@ } @media (max-width: 1200px) { - .heroRowTitle h1 { - font-size: clamp(72px, 18vw, 128px); - } - .heroRowTitle h1 span:last-child { margin-left: 0; } .heroPanel { - grid-template-columns: 1fr; - min-height: 838px; + width: min(100vw - var(--space-24) * 2, 1120px); + } +} + +@media (max-height: 820px) { + .heroRows { + gap: 16px; + } + + .heroRowBottom { + flex-wrap: wrap; + align-items: flex-end; + } + + .heroDescription { + max-width: 100%; + } + + .heroCtas { + margin-inline: 0; + } +} + +@media (max-height: 720px) { + .heroPanel { + padding-top: clamp(88px, 18svh, 140px); + } + + .heroRowTitle h1 { + line-height: 0.88; } } diff --git a/src/lander/sections/InterfaceSection.module.css b/src/lander/sections/InterfaceSection.module.css index 785a2af..f5b0b6a 100644 --- a/src/lander/sections/InterfaceSection.module.css +++ b/src/lander/sections/InterfaceSection.module.css @@ -1,7 +1,7 @@ .interfacePanel { - width: 1143px; + width: min(1143px, calc(100vw - var(--space-24) * 2)); border: 1px solid var(--c-surface-border); - padding: 232px 131px 78px; + padding: clamp(104px, 24svh, 232px) clamp(32px, 8vw, 131px) clamp(32px, 7svh, 78px); background: var(--c-surface-light); color: var(--c-bg-mid); } @@ -13,7 +13,7 @@ line-height: 1.05; color: var(--c-bg-mid); max-width: 785px; - font-size: 96px; + font-size: clamp(48px, min(8vw, 14svh), 96px); } .interfaceContent { @@ -25,21 +25,34 @@ .interfaceSub { margin-top: 24px; color: var(--c-bg-mid); - font: 400 36px/1.2 var(--font-display); + font: 400 clamp(24px, 4.8svh, 36px) / 1.2 var(--font-display); } .interfaceFoot { margin: 0 0 12px; text-align: right; color: var(--c-text-deep); - font: 400 16px/1.2 var(--font-body); + font: 400 clamp(14px, 2.1svh, 16px) / 1.2 var(--font-body); } @media (max-width: 1200px) { - .interfacePanel h2 { - font-size: clamp(48px, 12vw, 72px); - } .interfaceSub { font-size: 26px; } } + +@media (max-height: 760px) { + .interfaceContent { + grid-template-rows: auto auto 1fr; + gap: 14px; + } + + .interfaceSub { + margin-top: 0; + } + + .interfaceFoot { + align-self: start; + text-align: left; + } +} diff --git a/src/shared/components/GlassCard.module.css b/src/shared/components/GlassCard.module.css index 3fe5c8c..e07c7cf 100644 --- a/src/shared/components/GlassCard.module.css +++ b/src/shared/components/GlassCard.module.css @@ -8,27 +8,28 @@ background: linear-gradient(180deg, rgba(72, 176, 167, 0.24), rgba(44, 132, 126, 0.2)); box-shadow: var(--glass-shadow); backdrop-filter: blur(var(--glass-blur)); - padding: 42px 30% 42px 42px; + padding: clamp(24px, 4svh, 42px) clamp(18px, 18%, 30%) clamp(24px, 4svh, 42px) + clamp(24px, 4svh, 42px); min-height: 0; } .glassCard h3 { margin: 0; color: var(--c-text-light); - font: 600 40px/1 var(--font-display); + font: 600 clamp(28px, 5svh, 40px) / 1 var(--font-display); max-width: 90%; } .glassCard p { margin: 0; color: var(--c-text-light); - font: 400 18px/1.35 var(--font-body); + font: 400 clamp(16px, 2.2svh, 18px) / 1.35 var(--font-body); max-width: 96%; } .glassCardBody { align-self: end; - padding-top: 32px; + padding-top: clamp(18px, 3svh, 32px); } @media (max-width: 1200px) { From 4b89f3109eb5e475993d8882531213beedd6a66a Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 2 Apr 2026 20:34:35 -0400 Subject: [PATCH 2/4] works well but not in safari --- src/app/components/EventRail.module.css | 14 ++- src/app/components/EventRail.tsx | 35 ++++++ src/app/components/OrgRail.module.css | 14 ++- src/app/components/OrgRail.tsx | 35 ++++++ .../useHorizontalWheelScroll.test.tsx | 90 +++++++++++++- .../hooks/useHorizontalRailWheelScroll.ts | 49 ++++++++ src/shared/hooks/useHorizontalWheelScroll.ts | 111 +++++++++++++++--- 7 files changed, 326 insertions(+), 22 deletions(-) create mode 100644 src/shared/hooks/useHorizontalRailWheelScroll.ts diff --git a/src/app/components/EventRail.module.css b/src/app/components/EventRail.module.css index 8d8346d..acd94f4 100644 --- a/src/app/components/EventRail.module.css +++ b/src/app/components/EventRail.module.css @@ -85,6 +85,9 @@ scroll-behavior: smooth; scrollbar-width: none; align-items: stretch; + outline: none; + overscroll-behavior-x: contain; + touch-action: pan-x pinch-zoom; mask-image: linear-gradient( 90deg, transparent 0, @@ -101,6 +104,10 @@ ); } +.carousel:focus-visible { + box-shadow: inset 0 0 0 1px rgba(249, 250, 250, 0.28); +} + .carousel[data-can-scroll-back='false'] { --left-edge-fade: 0px; } @@ -113,8 +120,11 @@ display: none; } -.carousel:global(.is-dragging) { - cursor: grabbing; +@supports (-webkit-touch-callout: none) { + .carousel { + mask-image: none; + -webkit-mask-image: none; + } } @media (max-width: 1200px) { diff --git a/src/app/components/EventRail.tsx b/src/app/components/EventRail.tsx index 3e8e519..a678514 100644 --- a/src/app/components/EventRail.tsx +++ b/src/app/components/EventRail.tsx @@ -1,5 +1,7 @@ import { useEffect, useState, useRef } from 'react' +import type { KeyboardEvent } from 'react' import type { AppEvent } from '@/app/data/events' +import { useHorizontalRailWheelScroll } from '@/shared/hooks/useHorizontalRailWheelScroll' import { EventCard } from './EventCard' import styles from './EventRail.module.css' @@ -21,6 +23,8 @@ export function EventRail({ title, events, carouselLabel, onEventSelect }: Event const [canScrollForward, setCanScrollForward] = useState(false) const hasEvents = events.length > 0 + useHorizontalRailWheelScroll(railRef) + useEffect(() => { const rail = railRef.current if (!rail) return @@ -57,6 +61,34 @@ export function EventRail({ title, events, carouselLabel, onEventSelect }: Event }) } + const onRailKeyDown = (event: KeyboardEvent) => { + if (event.key === 'ArrowRight') { + event.preventDefault() + scrollRail('forward') + return + } + + if (event.key === 'ArrowLeft') { + event.preventDefault() + scrollRail('back') + return + } + + if (event.key === 'Home') { + event.preventDefault() + railRef.current?.scrollTo({ left: 0, behavior: 'smooth' }) + return + } + + if (event.key === 'End') { + const rail = railRef.current + if (!rail) return + + event.preventDefault() + rail.scrollTo({ left: rail.scrollWidth - rail.clientWidth, behavior: 'smooth' }) + } + } + return (
@@ -81,9 +113,12 @@ export function EventRail({ title, events, carouselLabel, onEventSelect }: Event ref={railRef} className={styles.carousel} data-reveal-scroller + data-native-horizontal-scroll aria-label={carouselLabel} + tabIndex={0} data-can-scroll-back={canScrollBack} data-can-scroll-forward={canScrollForward} + onKeyDown={onRailKeyDown} > {events.map((event) => ( diff --git a/src/app/components/OrgRail.module.css b/src/app/components/OrgRail.module.css index 3c2482a..08e260b 100644 --- a/src/app/components/OrgRail.module.css +++ b/src/app/components/OrgRail.module.css @@ -85,6 +85,9 @@ scroll-behavior: smooth; scrollbar-width: none; align-items: stretch; + outline: none; + overscroll-behavior-x: contain; + touch-action: pan-x pinch-zoom; mask-image: linear-gradient( 90deg, transparent 0, @@ -101,6 +104,10 @@ ); } +.carousel:focus-visible { + box-shadow: inset 0 0 0 1px rgba(249, 250, 250, 0.28); +} + .carousel[data-can-scroll-back='false'] { --left-edge-fade: 0px; } @@ -113,8 +120,11 @@ display: none; } -.carousel:global(.is-dragging) { - cursor: grabbing; +@supports (-webkit-touch-callout: none) { + .carousel { + mask-image: none; + -webkit-mask-image: none; + } } @media (max-width: 1200px) { diff --git a/src/app/components/OrgRail.tsx b/src/app/components/OrgRail.tsx index 1d393f2..10e96fe 100644 --- a/src/app/components/OrgRail.tsx +++ b/src/app/components/OrgRail.tsx @@ -1,5 +1,7 @@ import { useEffect, useRef, useState } from 'react' +import type { KeyboardEvent } from 'react' import type { AppOrganization } from '@/app/data/organizations' +import { useHorizontalRailWheelScroll } from '@/shared/hooks/useHorizontalRailWheelScroll' import { OrgCard } from './OrgCard' import styles from './OrgRail.module.css' @@ -26,6 +28,8 @@ export function OrgRail({ const [canScrollForward, setCanScrollForward] = useState(false) const hasOrganizations = organizations.length > 0 + useHorizontalRailWheelScroll(railRef) + useEffect(() => { const rail = railRef.current if (!rail) return @@ -62,6 +66,34 @@ export function OrgRail({ }) } + const onRailKeyDown = (event: KeyboardEvent) => { + if (event.key === 'ArrowRight') { + event.preventDefault() + scrollRail('forward') + return + } + + if (event.key === 'ArrowLeft') { + event.preventDefault() + scrollRail('back') + return + } + + if (event.key === 'Home') { + event.preventDefault() + railRef.current?.scrollTo({ left: 0, behavior: 'smooth' }) + return + } + + if (event.key === 'End') { + const rail = railRef.current + if (!rail) return + + event.preventDefault() + rail.scrollTo({ left: rail.scrollWidth - rail.clientWidth, behavior: 'smooth' }) + } + } + return (
@@ -86,9 +118,12 @@ export function OrgRail({ ref={railRef} className={styles.carousel} data-reveal-scroller + data-native-horizontal-scroll aria-label={carouselLabel} + tabIndex={0} data-can-scroll-back={canScrollBack} data-can-scroll-forward={canScrollForward} + onKeyDown={onRailKeyDown} > {organizations.map((organization) => ( { - function TestComponent() { + function defineScrollableMetrics( + node: HTMLElement, + values: { + clientWidth: number + scrollWidth: number + initialScrollLeft?: number + }, + ) { + let scrollLeft = values.initialScrollLeft ?? 0 + + Object.defineProperty(node, 'clientWidth', { + configurable: true, + get: () => values.clientWidth, + }) + + Object.defineProperty(node, 'scrollWidth', { + configurable: true, + get: () => values.scrollWidth, + }) + + Object.defineProperty(node, 'scrollLeft', { + configurable: true, + get: () => scrollLeft, + set: (value: number) => { + scrollLeft = value + }, + }) + } + + function TestComponent({ + releaseOnEdges = false, + ignoreInteractiveElements = true, + enableDrag = true, + }: { + releaseOnEdges?: boolean + ignoreInteractiveElements?: boolean + enableDrag?: boolean + }) { const ref = useRef(null) - useHorizontalWheelScroll(ref) + useHorizontalWheelScroll(ref, { + endCutoffPx: 0, + releaseOnEdges, + ignoreInteractiveElements, + enableDrag, + }) return (
- Test +
) } @@ -19,4 +61,44 @@ describe('useHorizontalWheelScroll', () => { const div = getByTestId('test-div') expect(div).toBeInTheDocument() }) + + it('handles wheel input while there is remaining horizontal space', () => { + const { getByTestId } = render() + const div = getByTestId('test-div') + defineScrollableMetrics(div, { clientWidth: 200, scrollWidth: 500, initialScrollLeft: 120 }) + + const eventHandled = fireEvent.wheel(div, { deltaY: 60, cancelable: true }) + + expect(eventHandled).toBe(false) + expect(div.scrollLeft).toBe(186) + }) + + it('releases wheel input at the rail edge when configured', () => { + const { getByTestId } = render() + const div = getByTestId('test-div') + defineScrollableMetrics(div, { clientWidth: 200, scrollWidth: 500, initialScrollLeft: 300 }) + + const eventHandled = fireEvent.wheel(div, { deltaY: 60, cancelable: true }) + + expect(eventHandled).toBe(true) + expect(div.scrollLeft).toBe(300) + }) + + it('does not activate drag handlers when drag support is disabled', () => { + const { getByRole, getByTestId } = render( + , + ) + const div = getByTestId('test-div') + const button = getByRole('button', { name: 'Card' }) + defineScrollableMetrics(div, { clientWidth: 200, scrollWidth: 500, initialScrollLeft: 120 }) + + const addSpy = jest.spyOn(div.classList, 'add') + fireEvent.mouseDown(button, { button: 0, clientX: 100 }) + fireEvent.mouseMove(window, { clientX: 70 }) + fireEvent.mouseUp(window) + + expect(addSpy).not.toHaveBeenCalled() + expect(div.scrollLeft).toBe(120) + addSpy.mockRestore() + }) }) diff --git a/src/shared/hooks/useHorizontalRailWheelScroll.ts b/src/shared/hooks/useHorizontalRailWheelScroll.ts new file mode 100644 index 0000000..8f397e4 --- /dev/null +++ b/src/shared/hooks/useHorizontalRailWheelScroll.ts @@ -0,0 +1,49 @@ +import { useEffect } from 'react' +import type { RefObject } from 'react' + +/** + * Handles explicit horizontal wheel input for nested rails. + * + * This consumes true horizontal deltas (`deltaX`) while allowing vertical wheel + * input to keep bubbling to the page-level horizontal scroller. + */ +export function useHorizontalRailWheelScroll( + railRef: RefObject, + speed = 1, +): void { + useEffect(() => { + const rail = railRef.current + if (!rail) return + + const onWheel = (event: WheelEvent) => { + const maxScrollLeft = Math.max(0, rail.scrollWidth - rail.clientWidth) + if (maxScrollLeft <= 0) { + return + } + + const horizontalIntent = event.deltaX + if (Math.abs(horizontalIntent) < 0.5) { + return + } + + const isAtStart = rail.scrollLeft <= 0 + const isAtEnd = rail.scrollLeft >= maxScrollLeft + + if ((horizontalIntent < 0 && isAtStart) || (horizontalIntent > 0 && isAtEnd)) { + return + } + + event.preventDefault() + event.stopPropagation() + + const next = rail.scrollLeft + horizontalIntent * speed + rail.scrollLeft = Math.min(maxScrollLeft, Math.max(0, next)) + } + + rail.addEventListener('wheel', onWheel, { passive: false }) + + return () => { + rail.removeEventListener('wheel', onWheel) + } + }, [railRef, speed]) +} diff --git a/src/shared/hooks/useHorizontalWheelScroll.ts b/src/shared/hooks/useHorizontalWheelScroll.ts index 59b18c0..ee5a30b 100644 --- a/src/shared/hooks/useHorizontalWheelScroll.ts +++ b/src/shared/hooks/useHorizontalWheelScroll.ts @@ -4,6 +4,9 @@ import type { RefObject } from 'react' type HorizontalWheelOptions = { speed?: number endCutoffPx?: number + releaseOnEdges?: boolean + ignoreInteractiveElements?: boolean + enableDrag?: boolean } /** @@ -13,17 +16,76 @@ type HorizontalWheelOptions = { * @param options - Configuration options for the scrolling behavior. * @param options.speed - The multiplier for scroll speed when using the mouse wheel (default: 1.1). * @param options.endCutoffPx - The number of pixels from the end of the scroll width to treat as the maximum scroll threshold (default: 180). + * @param options.releaseOnEdges - Allows parent scrollers to receive wheel input once this scroller reaches an edge (default: false). + * @param options.ignoreInteractiveElements - Prevents drag-to-scroll from starting on controls like buttons or inputs (default: true). + * @param options.enableDrag - Enables mouse drag-to-scroll interactions for the target scroller (default: true). */ export function useHorizontalWheelScroll( scrollerRef: RefObject, options: HorizontalWheelOptions = {}, ): void { - const { speed = 1.1, endCutoffPx = 180 } = options + const { + speed = 1.1, + endCutoffPx = 180, + releaseOnEdges = false, + ignoreInteractiveElements = true, + enableDrag = true, + } = options useEffect(() => { const scroller = scrollerRef.current if (!scroller) return + const getNestedHorizontalScroller = (event: WheelEvent) => { + const eventPath = typeof event.composedPath === 'function' ? event.composedPath() : [] + + for (const node of eventPath) { + if (!(node instanceof HTMLElement)) { + continue + } + + if (node === scroller) { + break + } + + if (node.hasAttribute('data-native-horizontal-scroll')) { + return node + } + } + + const target = event.target as HTMLElement | null + const nestedScroller = target?.closest( + '[data-native-horizontal-scroll]', + ) as HTMLElement | null + if (!nestedScroller || nestedScroller === scroller) { + return null + } + + return nestedScroller + } + + const canNestedScrollerConsume = (event: WheelEvent, intent: number) => { + const nestedScroller = getNestedHorizontalScroller(event) + if (!nestedScroller) { + return false + } + + const maxScrollLeft = Math.max(0, nestedScroller.scrollWidth - nestedScroller.clientWidth) + if (maxScrollLeft <= 0) { + return false + } + + if (intent < 0) { + return nestedScroller.scrollLeft > 0 + } + + if (intent > 0) { + return nestedScroller.scrollLeft < maxScrollLeft + } + + return false + } + const getMaxScrollLeft = () => Math.max(0, scroller.scrollWidth - scroller.clientWidth - endCutoffPx) @@ -45,9 +107,21 @@ export function useHorizontalWheelScroll( const intent = Math.abs(event.deltaY) > Math.abs(event.deltaX) ? event.deltaY : event.deltaX if (intent === 0) return + if (canNestedScrollerConsume(event, intent)) { + return + } + + const maxScrollLeft = getMaxScrollLeft() + const isAtStart = scroller.scrollLeft <= 0 + const isAtEnd = scroller.scrollLeft >= maxScrollLeft + + if (releaseOnEdges && ((intent < 0 && isAtStart) || (intent > 0 && isAtEnd))) { + return + } + event.preventDefault() + event.stopPropagation() const next = scroller.scrollLeft + intent * speed - const maxScrollLeft = getMaxScrollLeft() scroller.scrollLeft = Math.min(maxScrollLeft, Math.max(0, next)) } @@ -66,7 +140,9 @@ export function useHorizontalWheelScroll( if (event.button !== 0) return const target = event.target as HTMLElement | null - if (target?.closest('button, input, textarea, select, label')) return + if (ignoreInteractiveElements && target?.closest('button, input, textarea, select, label')) { + return + } isMouseDragging = true hasActivatedDrag = false @@ -125,20 +201,27 @@ export function useHorizontalWheelScroll( scroller.addEventListener('wheel', onWheel, { passive: false }) scroller.addEventListener('scroll', onScroll, { passive: true }) - scroller.addEventListener('mousedown', onMouseDown) - scroller.addEventListener('dragstart', onNativeDragStart) - scroller.addEventListener('click', onClickCapture, true) - window.addEventListener('mousemove', onMouseMove, { passive: false }) - window.addEventListener('mouseup', endMouseDrag) + if (enableDrag) { + scroller.addEventListener('mousedown', onMouseDown) + scroller.addEventListener('dragstart', onNativeDragStart) + scroller.addEventListener('click', onClickCapture, true) + window.addEventListener('mousemove', onMouseMove, { passive: false }) + window.addEventListener('mouseup', endMouseDrag) + } + return () => { scroller.removeEventListener('wheel', onWheel) scroller.removeEventListener('scroll', onScroll) - scroller.removeEventListener('mousedown', onMouseDown) - scroller.removeEventListener('dragstart', onNativeDragStart) - scroller.removeEventListener('click', onClickCapture, true) - window.removeEventListener('mousemove', onMouseMove) - window.removeEventListener('mouseup', endMouseDrag) + + if (enableDrag) { + scroller.removeEventListener('mousedown', onMouseDown) + scroller.removeEventListener('dragstart', onNativeDragStart) + scroller.removeEventListener('click', onClickCapture, true) + window.removeEventListener('mousemove', onMouseMove) + window.removeEventListener('mouseup', endMouseDrag) + } + scroller.classList.remove('is-dragging') } - }, [scrollerRef, speed, endCutoffPx]) + }, [scrollerRef, speed, endCutoffPx, releaseOnEdges, ignoreInteractiveElements, enableDrag]) } From f58db281891fe83174eb5c373ad9e4ee0407bb02 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 2 Apr 2026 20:41:05 -0400 Subject: [PATCH 3/4] works on chromium browsers --- src/app/components/EventRail.tsx | 4 +-- src/app/components/OrgRail.tsx | 4 +-- ....ts => useSafariHorizontalRailFallback.ts} | 27 ++++++++++++------- 3 files changed, 22 insertions(+), 13 deletions(-) rename src/shared/hooks/{useHorizontalRailWheelScroll.ts => useSafariHorizontalRailFallback.ts} (52%) diff --git a/src/app/components/EventRail.tsx b/src/app/components/EventRail.tsx index a678514..448cbaf 100644 --- a/src/app/components/EventRail.tsx +++ b/src/app/components/EventRail.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react' import type { KeyboardEvent } from 'react' import type { AppEvent } from '@/app/data/events' -import { useHorizontalRailWheelScroll } from '@/shared/hooks/useHorizontalRailWheelScroll' +import { useSafariHorizontalRailFallback } from '@/shared/hooks/useSafariHorizontalRailFallback' import { EventCard } from './EventCard' import styles from './EventRail.module.css' @@ -23,7 +23,7 @@ export function EventRail({ title, events, carouselLabel, onEventSelect }: Event const [canScrollForward, setCanScrollForward] = useState(false) const hasEvents = events.length > 0 - useHorizontalRailWheelScroll(railRef) + useSafariHorizontalRailFallback(railRef) useEffect(() => { const rail = railRef.current diff --git a/src/app/components/OrgRail.tsx b/src/app/components/OrgRail.tsx index 10e96fe..412808b 100644 --- a/src/app/components/OrgRail.tsx +++ b/src/app/components/OrgRail.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import type { KeyboardEvent } from 'react' import type { AppOrganization } from '@/app/data/organizations' -import { useHorizontalRailWheelScroll } from '@/shared/hooks/useHorizontalRailWheelScroll' +import { useSafariHorizontalRailFallback } from '@/shared/hooks/useSafariHorizontalRailFallback' import { OrgCard } from './OrgCard' import styles from './OrgRail.module.css' @@ -28,7 +28,7 @@ export function OrgRail({ const [canScrollForward, setCanScrollForward] = useState(false) const hasOrganizations = organizations.length > 0 - useHorizontalRailWheelScroll(railRef) + useSafariHorizontalRailFallback(railRef) useEffect(() => { const rail = railRef.current diff --git a/src/shared/hooks/useHorizontalRailWheelScroll.ts b/src/shared/hooks/useSafariHorizontalRailFallback.ts similarity index 52% rename from src/shared/hooks/useHorizontalRailWheelScroll.ts rename to src/shared/hooks/useSafariHorizontalRailFallback.ts index 8f397e4..d61fa9b 100644 --- a/src/shared/hooks/useHorizontalRailWheelScroll.ts +++ b/src/shared/hooks/useSafariHorizontalRailFallback.ts @@ -1,19 +1,28 @@ import { useEffect } from 'react' import type { RefObject } from 'react' +function isSafariBrowser() { + if (typeof navigator === 'undefined') { + return false + } + + const userAgent = navigator.userAgent + return /Safari/i.test(userAgent) && !/Chrome|Chromium|CriOS|EdgiOS|FxiOS/i.test(userAgent) +} + /** - * Handles explicit horizontal wheel input for nested rails. + * Adds a Safari-only wheel fallback for nested horizontal rails. * - * This consumes true horizontal deltas (`deltaX`) while allowing vertical wheel - * input to keep bubbling to the page-level horizontal scroller. + * Safari can fail to hand trackpad and wheel gestures to nested horizontal + * overflow containers when a parent also participates in scroll handling. */ -export function useHorizontalRailWheelScroll( +export function useSafariHorizontalRailFallback( railRef: RefObject, speed = 1, ): void { useEffect(() => { const rail = railRef.current - if (!rail) return + if (!rail || !isSafariBrowser()) return const onWheel = (event: WheelEvent) => { const maxScrollLeft = Math.max(0, rail.scrollWidth - rail.clientWidth) @@ -21,22 +30,22 @@ export function useHorizontalRailWheelScroll( return } - const horizontalIntent = event.deltaX - if (Math.abs(horizontalIntent) < 0.5) { + const intent = Math.abs(event.deltaX) > 0 ? event.deltaX : event.deltaY + if (intent === 0) { return } const isAtStart = rail.scrollLeft <= 0 const isAtEnd = rail.scrollLeft >= maxScrollLeft - if ((horizontalIntent < 0 && isAtStart) || (horizontalIntent > 0 && isAtEnd)) { + if ((intent < 0 && isAtStart) || (intent > 0 && isAtEnd)) { return } event.preventDefault() event.stopPropagation() - const next = rail.scrollLeft + horizontalIntent * speed + const next = rail.scrollLeft + intent * speed rail.scrollLeft = Math.min(maxScrollLeft, Math.max(0, next)) } From ee52e99725e6d2b3658cb99a622c5a221f6689b8 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 2 Apr 2026 20:58:06 -0400 Subject: [PATCH 4/4] works on chrome browsers, just use that for demo --- src/app/sections/EventsSection.module.css | 7 ++++--- src/app/sections/OrgsSection.module.css | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/sections/EventsSection.module.css b/src/app/sections/EventsSection.module.css index 4731468..afb674c 100644 --- a/src/app/sections/EventsSection.module.css +++ b/src/app/sections/EventsSection.module.css @@ -3,7 +3,7 @@ display: grid; grid-template-rows: auto auto minmax(0, auto); align-content: start; - gap: clamp(1.25rem, 3svh, 2rem); + gap: clamp(0.875rem, 2.2svh, 2rem); padding: clamp(3rem, 8svh, 5rem) 0 clamp(2rem, 9svh, 6rem) clamp(1.5rem, 5vw, 5rem); height: 100%; box-sizing: border-box; @@ -60,7 +60,7 @@ @media (max-width: 1200px) { .eventsPanel { - gap: 1.75rem; + gap: 1.25rem; padding: 2.5rem 0 5rem 2.75rem; } @@ -79,7 +79,7 @@ @media (max-width: 720px) { .eventsPanel { - gap: 1.25rem; + gap: 0.875rem; padding: 2rem 0 2.5rem 1.5rem; } @@ -108,6 +108,7 @@ @media (max-height: 760px) { .eventsPanel { + gap: 0.875rem; padding-top: clamp(2rem, 5svh, 3rem); padding-bottom: 1.5rem; } diff --git a/src/app/sections/OrgsSection.module.css b/src/app/sections/OrgsSection.module.css index e2a31dd..f7b995d 100644 --- a/src/app/sections/OrgsSection.module.css +++ b/src/app/sections/OrgsSection.module.css @@ -3,7 +3,7 @@ display: grid; grid-template-rows: auto auto minmax(0, auto); align-content: start; - gap: clamp(1.25rem, 3svh, 2rem); + gap: clamp(0.875rem, 2.2svh, 2rem); padding: clamp(3rem, 8svh, 5rem) 0 clamp(2rem, 9svh, 6rem) clamp(1.5rem, 5vw, 5rem); height: 100%; box-sizing: border-box; @@ -45,7 +45,7 @@ @media (max-width: 1200px) { .orgsPanel { - gap: 1.75rem; + gap: 1.25rem; padding: 2.5rem 0 5rem 2.75rem; } @@ -64,7 +64,7 @@ @media (max-width: 720px) { .orgsPanel { - gap: 1.25rem; + gap: 0.875rem; padding: 2rem 0 2.5rem 1.5rem; } @@ -89,6 +89,7 @@ @media (max-height: 760px) { .orgsPanel { + gap: 0.875rem; padding-top: clamp(2rem, 5svh, 3rem); padding-bottom: 1.5rem; }