+ {/* 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