# Stage 5b — post-run sticky-ATC + size-sheet patch

**Authored:** 2026-05-12 during brand-owner taste review of the deployed prototype.
**Status:** Bundle's `prototype/index.html` patched in place; behavior now matches the canonical DreamCloud reference.

## The drifts

Three issues surfaced when the deployed prototype was reviewed at the live URL (originally `redesign-runs.huxapps.com/stage-5b-natural-harmony-pdp/`, canonicalized 2026-05-12 to `shopify-runs.huxapps.com/2026-05-11-stage-5b-natural-harmony-pdp/`):

### D1 — Sticky-ATC fires at scroll=0

The desktop sticky top-bar + in-page-nav, the mobile sticky top-strip, and the mobile sticky bottom-footer all activated at page load — before the user had scrolled to or past the inline Add To Cart button. The original gate was `setStickyShown(!entry.isIntersecting)` inside an IntersectionObserver callback. On viewports where the inline ATC sits below the fold (mobile 375×812 → ATC at y=1498; desktop 1280×800 → ATC at y=875), the inline ATC is "not intersecting" at scroll=0, so the gate fires prematurely.

DreamCloud's verified behavior (per `research/inspiration-captures/dreamcloud-sticky-atc-2026-05-11/findings.md`) is "show only after the user has scrolled PAST the inline ATC, i.e. it has moved entirely above the top of the viewport." Below-the-fold-but-not-reached-yet and currently-visible-in-viewport should both keep the sticky hidden.

A first patch attempt (`scrolledPast = entry.boundingClientRect.top < 0; setStickyShown(scrolledPast && !entry.isIntersecting)`) was correct in intent but unreliable in practice — IntersectionObserver only fires on intersection state changes, not on every positional move. When the user scrolled quickly past the inline ATC, the observer could miss the transition entirely, leaving the stickies stuck in the wrong state.

The final fix replaces the IntersectionObserver with a scroll listener throttled to `requestAnimationFrame`. The listener computes `inlineAtc.getBoundingClientRect().bottom < 0` on every animation frame — robust against any scroll velocity, viewport size, or layout reflow. Initial state runs synchronously on script load. A `resize` listener mirrors the scroll listener for completeness.

### D2 — Mobile size bottom-sheet visible at rest

Two CSS bugs layered:

1. **Backdrop overridden by viewport-gating class.** `.sheet-backdrop { display: none }` was being overridden by `.show-mobile { display: block }` (defined later in the cascade, applied to the same element). The backdrop appeared on every mobile page load, dimming the entire viewport behind a non-existent open sheet.

2. **Sheet sliver poking above the sticky footer.** `.bottom-sheet { transform: translateY(100%) }` only shifted the sheet by its own height. With `bottom: 68px` anchoring it above the sticky footer, the result was a 68-px sliver of the sheet's header peeking above the footer at rest. Visually broken even when the sheet was "closed."

Both fixes:

- Remove `.show-mobile` from `.sheet-backdrop` and `.bottom-sheet` element class lists (the backdrop is `position: fixed` so doesn't need viewport-gating; the sheet is only triggered by the mobile-size chip which is itself viewport-gated).
- Change the sheet's resting transform to `translateY(calc(100% + 68px))` to also account for the bottom anchor.

### New finding — Sheet header lacked context

A brand-owner taste-review observation: when the size sheet opens, the user wants to see both the size they're picking AGAINST (current selection) and the size + price they're picking BETWEEN (per-tile). The deployed prototype already rendered per-tile pricing (label + current price on every tile), but the sheet header showed only `Select Size` + close X — no anchoring context.

Fix: expanded the sheet header to a two-line block:

```
Select Size
180 × 200 cm  |  Dhs. 6,742  Dhs. 5,394
```

The second line lives in `#sheet-current-summary` and is kept in sync by `selectSize(idx)` alongside the inline trigger, mobile chip, and desktop sticky-bar elements.

## Files patched

Only `prototype/index.html`. Five edits in one pass:

1. **JS — sticky-ATC gate.** IntersectionObserver replaced with rAF-throttled scroll listener; resize listener added for layout-change robustness.
2. **CSS — bottom-sheet transform.** `translateY(100%)` → `translateY(calc(100% + 68px))`.
3. **HTML — backdrop element.** Removed `.show-mobile` class.
4. **HTML — bottom-sheet element.** Removed `.show-mobile` class.
5. **HTML — sheet header + JS — selectSize().** Added size + compare-at + current price summary line; selectSize() now syncs it.

`design/schema.jsonld`, `design/copy.html`, `design/wireframe.html`, and other artifacts unchanged. The verdict (APPROVED) stands; the prototype's behavior is now consistent with what the verdict assessed.

## Verification

Pre-redeploy, browser-verified via `mcp__Claude_Preview` at both viewports:

| State | Mobile (375×812) | Desktop (1280×800) |
|---|---|---|
| scroll=0 (inline ATC below fold) | sticky-top + sticky-bot + backdrop + sheet all hidden ✓ | desktop-sticky hidden ✓ |
| inline ATC centered in viewport | stickies hidden ✓ | desktop-sticky hidden ✓ |
| scrolled past inline ATC | sticky-top + sticky-bot shown ✓ | desktop-sticky shown ✓ |
| tap mobile size chip | sheet slides up, backdrop dims, header summary shows selected size/price, 12 tiles render with label+price ✓ | (N/A on desktop) |

## Where to find this fix in the codebase

The structural fixes are already codified for FUTURE runs in:

- `stores/huxberry.yaml.sticky_atc_pattern.mobile.activation` — disambiguated activation contract with canonical implementation
- `stores/huxberry.yaml.sticky_atc_pattern.mobile.bottom_footer.size_trigger_behavior.bottom_sheet` — four-rule hide-state contract for modals
- `.claude/agents/prototype-builder.md` step 3a — sticky-ATC + modal hide-state + viewport-class purity rules + self-check evals

This patch retroactively brings the Stage 5b deployed prototype in line with what those rules now require — closing the gap between "documented for future runs" and "fixed in the live artifact brand owner is reviewing."

## Sibling notes

- `POLICY-DRIFT-NOTE.md` — storewide sleep-trial policy supersedes two `{{ claim-needed }}` placeholders in the bundle.
- `FAQ-PATCH-NOTE.md` — FAQ-reconciliation patch (schema/copy alignment to canonical 8-Q set).
- This note (`STICKY-ATC-PATCH-NOTE.md`) — sticky-ATC + size-sheet behavior patched in place.
