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 @@