From 21718e5e3593614f92c407df6dec4b6ea59b0d07 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 3 Nov 2026 14:46:55 +0200 Subject: [PATCH] Fix project stack: opaque recede, nav clearance and intro overlap - The receding card must stay fully opaque; dimming it let the card behind ghost through the title and chips on photo-led cards. - Give the pinned stage top padding so the section label clears the fixed nav, and a gap so the covered card does not clip the intro. - Let the deck size to the space left in the stage, so it can never overflow the viewport. --- app/src/components/ProjectStack.tsx | 31 +++++++++++-------- app/src/lib/stack.test.ts | 4 +-- app/src/lib/stack.ts | 4 ++- .../2026-11-03-mega-project-log-stack.md | 8 +++-- ...026-11-03-mega-project-log-stack-design.md | 4 ++- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app/src/components/ProjectStack.tsx b/app/src/components/ProjectStack.tsx index ee55fe5..e2d28c4 100644 --- a/app/src/components/ProjectStack.tsx +++ b/app/src/components/ProjectStack.tsx @@ -104,20 +104,25 @@ export function ProjectStack() { return (
-
+ {/* The header sits clear of the fixed nav, and the deck takes the rest of + the stage, so the card can never overflow the viewport. The gap keeps + the covered card — which lifts as it recedes — off the intro copy. */} +
-
- {projects.map((p, i) => ( -
{ - cardRefs.current[i] = el - }} - className="absolute inset-0 opacity-0 will-change-[transform,opacity]" - > - -
- ))} +
+
+ {projects.map((p, i) => ( +
{ + cardRefs.current[i] = el + }} + className="absolute inset-0 opacity-0 will-change-[transform,opacity]" + > + +
+ ))} +
diff --git a/app/src/lib/stack.test.ts b/app/src/lib/stack.test.ts index d9745c5..985bcfc 100644 --- a/app/src/lib/stack.test.ts +++ b/app/src/lib/stack.test.ts @@ -44,10 +44,10 @@ test('the first card has landed after one arrival unit', () => { assert.equal(s.rotate, 0) }) -test('a covered card eases back', () => { +test('a covered card eases back but stays opaque', () => { const s = cardState(0, ARRIVE + RECEDE) assert.equal(s.yPercent, -7) - assert.equal(s.opacity, 0.75) + assert.equal(s.opacity, 1) assert.equal(s.scale, 0.92) }) diff --git a/app/src/lib/stack.ts b/app/src/lib/stack.ts index 21ef751..5b434c0 100644 --- a/app/src/lib/stack.ts +++ b/app/src/lib/stack.ts @@ -55,7 +55,9 @@ export function cardState(index: number, t: number): CardState { return { yPercent: 118 * (1 - u) - 7 * v, - opacity: u * (1 - 0.25 * v), + // The recede only shifts and shrinks the card — it must stay fully opaque. + // A translucent card would let the one behind it ghost through the copy. + opacity: u, scale: 0.93 + 0.07 * u - 0.08 * v, rotate: noNegativeZero(tilt * (1 - u)), } diff --git a/docs/superpowers/plans/2026-11-03-mega-project-log-stack.md b/docs/superpowers/plans/2026-11-03-mega-project-log-stack.md index ca6c5f4..97ba0cb 100644 --- a/docs/superpowers/plans/2026-11-03-mega-project-log-stack.md +++ b/docs/superpowers/plans/2026-11-03-mega-project-log-stack.md @@ -107,10 +107,10 @@ test('the first card has landed after one arrival unit', () => { assert.equal(s.rotate, 0) }) -test('a covered card eases back', () => { +test('a covered card eases back but stays opaque', () => { const s = cardState(0, ARRIVE + RECEDE) assert.equal(s.yPercent, -7) - assert.equal(s.opacity, 0.75) + assert.equal(s.opacity, 1) assert.equal(s.scale, 0.92) }) @@ -211,7 +211,9 @@ export function cardState(index: number, t: number): CardState { return { yPercent: 118 * (1 - u) - 7 * v, - opacity: u * (1 - 0.25 * v), + // The recede must not change opacity: a translucent card lets the card + // behind it ghost through the title and chips. + opacity: u, scale: 0.93 + 0.07 * u - 0.08 * v, rotate: noNegativeZero(tilt * (1 - u)), } diff --git a/docs/superpowers/specs/2026-11-03-mega-project-log-stack-design.md b/docs/superpowers/specs/2026-11-03-mega-project-log-stack-design.md index ad9a88a..a403450 100644 --- a/docs/superpowers/specs/2026-11-03-mega-project-log-stack-design.md +++ b/docs/superpowers/specs/2026-11-03-mega-project-log-stack-design.md @@ -128,13 +128,15 @@ u = power2Out(u) // 1 - (1 - x)² v = power1InOut(v) // 2x² below 0.5, else 1 - 2(1-x)²/2 yPercent = 118 × (1 - u) - 7 × v -opacity = u × (1 - 0.25 × v) +opacity = u scale = 0.93 + 0.07 × u - 0.08 × v rotate = (i even ? -4° : 4°) × (1 - u) ``` `transform` is composed as `translate3d(0, yPercent%, 0) rotate(rotate) scale(scale)` and written to `element.style.transform`. `opacity` is written to `element.style.opacity`. +**The recede must not change opacity.** A covered card stays fully opaque — only its scale and position change. A translucent card would let the card behind it ghost through the title and chips, which is very visible on photo-led cards. The reference gets away with it because its cards sit on an opaque panel. + Cards later in the DOM paint above earlier ones, so the last card dealt ends up on top — the same stacking order as the reference. ### The active card