diff --git a/DESIGN.md b/DESIGN.md index ce4cb71..198c90a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -161,7 +161,7 @@ Known divergence is limited to content density: the visit page can use a shorter ### Visit Landing Hero - **Structure**: compact brand link, left-aligned value proposition, direct in-page CTAs, and a small proof/pass panel. -- **Layout**: page-grid composition with a dominant form and supporting notes rail on desktop; one-column source order on mobile. The hero keeps its two CTAs in one horizontal row and compresses the Cafe pass into a horizontal flow so the guestbook card is visible before scrolling on mobile. +- **Layout**: page-grid composition with a dominant form and supporting notes rail on desktop; one-column source order on mobile. The hero keeps its two CTAs in one horizontal row and renders the Cafe pass as a single-line, transform-only marquee so the steps never wrap into a taller block on mobile or desktop. - **States**: CTAs use the primary/secondary button states and visible focus rings. - **Accessibility**: hero actions link to real page sections, and the source order stays brand, promise, action, form, notes. @@ -169,7 +169,7 @@ Known divergence is limited to content density: the visit page can use a shorter - **Structure**: fixed bottom `aside`, centered divider-style label, reel viewport, and repeated brand asset cards. - **Desktop layout**: large horizontal brand cards inside the central page track with a slow continuous reel so sponsor assets read as real placements. - **Mobile layout**: each brand asset owns one full viewport width and advances every 2 seconds. -- **Logo treatment**: real sponsor image assets fill compact glass cards; light-background logo files may receive a small image radius while the rail height stays fixed. +- **Logo treatment**: real sponsor image assets fill compact glass cards; light-background logo files sit on a matching bright porcelain card and shelf while the rail height stays fixed. - **Overlap safety**: page and anchor scrolling reserve `--sponsor-rail-height` plus safe-area inset so the fixed footer never hides the guestbook CTA or note content. - **Motion**: transform-only animation, pauses on hover/focus, and stops under `prefers-reduced-motion`. diff --git a/tests/visit-page.test.mjs b/tests/visit-page.test.mjs index 0a802e3..8edd3e5 100644 --- a/tests/visit-page.test.mjs +++ b/tests/visit-page.test.mjs @@ -85,9 +85,14 @@ assert.match(html, /class=["']hero-actions["'][\s\S]*Sign guestbook[\s\S]*Browse assert.match(css, /\.hero-actions\b[\s\S]*flex-wrap:\s*nowrap/, 'visit hero CTAs should stay in one horizontal row'); assert.match(css, /\.hero-button\b[\s\S]*flex:\s*1 1 0/, 'visit hero CTAs should share a single row on mobile'); assert.doesNotMatch(css, /@media\s+\(max-width:\s*640px\)[\s\S]*\.hero-actions\s*\{[^}]*display:\s*grid/, 'mobile hero CTAs should not stack into a vertical grid'); -assert.match(css, /\.hero-pass\b[\s\S]*display:\s*flex[\s\S]*overflow-x:\s*auto/, 'hero pass should become a horizontal flow to reduce height'); -assert.match(css, /\.hero-pass ol\b[\s\S]*display:\s*flex/, 'hero pass steps should flow horizontally'); +assert.match(html, /
  • 1<\/span>Write a note<\/strong><\/li>/, 'hero pass should duplicate its steps for a seamless visual marquee without repeating them to assistive tech'); +assert.match(css, /\.hero-pass\b[\s\S]*display:\s*grid[\s\S]*overflow:\s*hidden/, 'hero pass should keep the moving track clipped inside one row'); +assert.match(html, /class=["']pass-marquee["'][\s\S]*
      /, 'hero pass should wrap the moving list in a clipped marquee viewport'); +assert.match(css, /\.pass-marquee\s*\{[^}]*overflow:\s*hidden/, 'hero pass marquee viewport should clip the animated list away from the label'); +assert.match(css, /\.hero-pass ol\s*\{[^}]*display:\s*flex[^}]*flex-wrap:\s*nowrap[^}]*animation:\s*pass-marquee/, 'hero pass steps should move as a single-line marquee'); +assert.match(css, /@keyframes\s+pass-marquee[\s\S]*translateX\(-50%\)/, 'hero pass marquee should loop over the duplicated track'); assert.match(css, /\.hero-pass li:not\(:last-child\)::after[\s\S]*linear-gradient/, 'hero pass flow should visually connect steps across the row'); +assert.doesNotMatch(css, /\.hero-pass ol\s*\{[^}]*flex-wrap:\s*wrap/, 'hero pass track should not wrap into multiple rows'); assert.match(html, /class=["'][^"']*guestbook-card[^"']*glass-panel[^"']*["'][^>]+id=["']formScreen["']/, 'visit form should reuse the shared glass-panel primitive'); assert.match(html, /class=["'][^"']*guestbook-card[^"']*glass-panel[^"']*["'][^>]+id=["']successScreen["']/, 'visit success state should reuse the shared glass-panel primitive'); assert.match(html, /class=["'][^"']*entries-card[^"']*glass-panel[^"']*["']/, 'visit guestbook wall should reuse the shared glass-panel primitive'); @@ -230,6 +235,8 @@ for (const sponsor of visitModule.SPONSORS) { assert.match(js, /id:\s*['"]dalpha['"][\s\S]*logo:\s*['"]assets\/logos\/dalpha-logo\.png['"]/, 'Dalpha should use the supplied footer logo image'); assert.match(js, /id:\s*['"]minds['"][\s\S]*logo:\s*['"]assets\/logos\/minds-logo\.png['"]/, 'Minds should use the supplied footer logo image'); assert.match(css, /\.sponsor-card\[data-sponsor=["']arize["']\] img[\s\S]*border-radius:/, 'Arize logo image should receive a rounded image edge'); +assert.match(css, /\.sponsor-carousel\b[\s\S]*rgba\(255, 255, 255, 0\.13\)/, 'sponsor footer shelf should pick up the bright logo backgrounds'); +assert.match(css, /\.sponsor-card\[data-sponsor=["']arize["']\],[\s\S]*\.sponsor-card\[data-sponsor=["']minds["']\]\s*\{[\s\S]*rgba\(255, 255, 255, 0\.92\)/, 'Arize and Minds should sit on matching bright sponsor cards'); assert.match(css, /\.sponsor-card\.has-logo\b/, 'sponsor logo cards should expose a logo-specific style hook'); assert.match(css, /sponsor-mobile-cycle[\s\S]*8s[\s\S]*100vw/, 'mobile sponsor carousel should rotate one full-viewport brand asset every two seconds'); assert.match(css, /\.sponsor-carousel:hover \.sponsor-track|\.sponsor-carousel:focus-within \.sponsor-track/, 'sponsor carousel should pause on hover or focus'); diff --git a/visit.css b/visit.css index 9afcad1..f244995 100644 --- a/visit.css +++ b/visit.css @@ -372,53 +372,76 @@ h2 { } .hero-pass { - display: flex; + position: relative; + display: grid; align-items: center; - gap: 0.75rem; + grid-template-columns: auto minmax(0, 1fr); + gap: 0.82rem; width: min(100%, 52rem); - min-width: 0; - overflow-x: auto; + min-inline-size: 0; + overflow: hidden; padding: 0.78rem 0.85rem; border: 1px solid rgba(152, 225, 255, 0.2); border-radius: var(--radius-lg); background: rgba(9, 10, 30, 0.36); box-shadow: inset 0 0 24px rgba(152, 225, 255, 0.05); - scrollbar-width: none; } -.hero-pass::-webkit-scrollbar { - display: none; +.hero-pass::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 3rem; + pointer-events: none; + content: ""; + background: linear-gradient(90deg, transparent, rgba(9, 10, 30, 0.88)); } .pass-label { - flex: 0 0 auto; color: var(--accent-secondary); font-family: "Space Grotesk", system-ui, sans-serif; font-size: 0.78rem; font-weight: 800; + line-height: 1; white-space: nowrap; } +.pass-marquee { + min-inline-size: 0; + overflow: hidden; +} + .hero-pass ol { display: flex; + flex-wrap: nowrap; align-items: center; gap: 0.5rem; + width: max-content; min-width: max-content; margin: 0; padding: 0; + animation: pass-marquee 22s linear infinite; list-style: none; + will-change: transform; +} + +.hero-pass:hover ol { + animation-play-state: paused; } .hero-pass li { position: relative; display: inline-flex; - gap: 0.45rem; + flex: 0 0 auto; align-items: center; + gap: 0.42rem; color: var(--text-secondary); + white-space: nowrap; } .hero-pass li:not(:last-child)::after { - width: 1.35rem; + width: 1.45rem; height: 1px; margin-left: 0.15rem; content: ""; @@ -442,6 +465,7 @@ h2 { .hero-pass li strong { color: var(--text-primary); font-size: 0.82rem; + line-height: 1.22; white-space: nowrap; } @@ -1054,8 +1078,16 @@ textarea:focus { } .sponsor-carousel { - scrollbar-width: none; min-width: 0; + border: 1px solid rgba(255, 255, 255, 0.13); + border-radius: var(--radius-md); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.13), rgba(232, 238, 255, 0.06)), + rgba(8, 8, 29, 0.18); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.12), + inset 0 -18px 32px rgba(8, 8, 29, 0.14); + scrollbar-width: none; } .sponsor-carousel::-webkit-scrollbar { @@ -1128,11 +1160,21 @@ textarea:focus { border-radius: calc(var(--radius-sm) - 0.12rem); } +.sponsor-card[data-sponsor="arize"], .sponsor-card[data-sponsor="minds"] { padding-block: 0.28rem; background: - linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(231, 238, 255, 0.82)), - rgba(255, 255, 255, 0.86); + linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(235, 240, 255, 0.88)), + rgba(255, 255, 255, 0.92); + border-color: rgba(255, 255, 255, 0.42); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.78), + inset 0 -16px 28px rgba(55, 67, 113, 0.08), + 0 10px 24px rgba(0, 0, 0, 0.18); +} + +.sponsor-card[data-sponsor="arize"] img { + height: 2.8rem; } .sponsor-card[data-sponsor="minds"] img { @@ -1158,6 +1200,16 @@ textarea:focus { } } +@keyframes pass-marquee { + from { + transform: translateX(0); + } + + to { + transform: translateX(-50%); + } +} + @keyframes sponsor-mobile-cycle { 0%, 20% { @@ -1195,9 +1247,14 @@ textarea:focus { scroll-behavior: auto; } + .hero-pass ol, .sponsor-track { animation: none; } + + .hero-pass ol { + transform: none; + } } @media (max-width: 860px) { @@ -1262,10 +1319,19 @@ textarea:focus { } .hero-pass { + grid-template-columns: auto minmax(0, 1fr); width: 100%; padding: 0.56rem 0.65rem; } + .hero-pass ol { + animation-duration: 18s; + } + + .hero-pass li strong { + font-size: 0.76rem; + } + .guestbook-card, .entries-card { padding: 0.72rem; @@ -1377,6 +1443,10 @@ textarea:focus { height: 2.35rem; } + .sponsor-card[data-sponsor="arize"] img { + height: 2.72rem; + } + .sponsor-card[data-sponsor="minds"] img { height: 2.95rem; } diff --git a/visit.html b/visit.html index 0d8dba5..a0a4bc1 100644 --- a/visit.html +++ b/visit.html @@ -54,11 +54,16 @@

      Meet the cafe crowd.