diff --git a/site/css/sections.css b/site/css/sections.css
index 1c6b6dc..345f7f7 100644
--- a/site/css/sections.css
+++ b/site/css/sections.css
@@ -198,22 +198,26 @@
overflow-x: auto;
overflow-y: visible;
scroll-snap-type: x mandatory;
- /* Full-bleed: escape both the page max-width AND the page gutter so
- the rail spans the full viewport regardless of how wide the
- screen is. Cards extend right up to the viewport's edge. */
+ /* Full-bleed: rail spans the full viewport. But the FIRST card
+ starts aligned with the page content (same left edge as the
+ section header), not flush against the viewport. As the user
+ scrolls horizontally, cards travel through the full bleed
+ including the viewport edges — only the initial position
+ respects the page's content gutter. */
width: 100vw;
margin-inline: calc(50% - 50vw);
- padding-inline: var(--page-gutter);
+ --rail-edge: max(var(--page-gutter), calc((100vw - var(--page-max)) / 2 + var(--page-gutter)));
+ padding-inline: var(--rail-edge);
padding-block: var(--space-md) var(--space-lg);
- scroll-padding-inline: var(--page-gutter);
+ scroll-padding-inline: var(--rail-edge);
scrollbar-color: var(--color-rule-2) transparent;
}
/* Reserve trailing whitespace inside the rail so the last card snaps
- clean to the gutter and isn't cut off by the bleed margin. */
+ clean to the same content edge as the first. */
.examples-rail::after {
content: "";
- flex: 0 0 var(--page-gutter);
+ flex: 0 0 var(--rail-edge);
}
.examples-rail::-webkit-scrollbar { height: 6px; }
diff --git a/skill/references/component-cookbook.md b/skill/references/component-cookbook.md
index 00aee63..b6de94b 100644
--- a/skill/references/component-cookbook.md
+++ b/skill/references/component-cookbook.md
@@ -876,6 +876,24 @@ Wordmark hard-left, single CTA hard-right, vast empty space between, no link row
*Anti-pattern:* adding 4 inline links between the wordmark and CTA "to fill the space". The space *is* the design; if you fill it, you've made N1 with extra steps.
+### N10 · Floating-on-scroll morph
+A sticky bar at the top that **morphs into a floating pill** as the user scrolls past a threshold. Two visual modes share one DOM — `.nav` (outer) owns the bar look, `.nav__inner` (inner) owns the pill look. Cross-faded on a single class toggle (`.is-floating`) with one timing curve. Active layer feels seamless; AI defaults always botch this.
+*Use when:* atmospheric / modern-minimal pages where the kinetic micro-moment earns its place. Adds a single tasteful surprise; resists novelty.
+*Don't confuse with:* N5 Floating pill (always-on, no scroll behaviour). N10 is N5 plus a default-bar state that morphs *into* it.
+
+```html
+
+```
+
+The full recipe — the four laws (height-constant, transform-for-offset, cross-fade-everything, single-curve), the property-morph table, the scroll-handler script, and the eight anti-patterns Hallmark refuses — lives in [`floating-nav.md`](floating-nav.md). Reach for that file *before* building this archetype. Skipping the four laws is what makes 90% of attempts read as broken.
+
+*Anti-pattern (one of eight in floating-nav.md):* swapping two `` elements via opacity instead of cross-fading one DOM. Doubles markup, fights focus order, desyncs content.
+
---
## Routing — which nav fits which genre / theme
diff --git a/skill/references/floating-nav.md b/skill/references/floating-nav.md
new file mode 100644
index 0000000..a4c0033
--- /dev/null
+++ b/skill/references/floating-nav.md
@@ -0,0 +1,89 @@
+# Floating nav on scroll — the cross-fade morph
+
+The recipe for **N10 · Floating-on-scroll morph** (see [`component-cookbook.md` § Navigation](component-cookbook.md)). One DOM, two visual modes, single class toggle, one timing curve. AI defaults botch every one of the four laws below — which is why N10 is the most demanding nav in the cookbook.
+
+## The structure
+
+One `` with an inner wrapper. The outer owns the **default bar** visuals; the inner owns the **floating pill** visuals. As `.is-floating` toggles past a scroll threshold, each layer cross-fades its own visuals out while the other fades in.
+
+```html
+
+```
+
+## The four laws — non-negotiable
+
+**1. Total nav height stays constant.** If outer height changes when state flips, every pixel below shifts vertically. Users perceive this as "the page jumped" mid-scroll. Compensate the inner's shrink with the outer's `padding-block` so the math sums to the same total in both states.
+
+**2. Visible offset uses `transform: translateY()`, never `padding`/`margin`.** The pill should sit detached from the viewport top with breathing room. The naïve add-padding-to-outer fix breaks Law 1. `transform` doesn't affect layout — pill drops visually, box stays put.
+
+**3. Cross-fade ownership of every shared visual.** For every property the outer carries in default (background, border, backdrop-filter, box-shadow), explicitly neutralise it in `.is-floating` *and* put it in the transition list. Forgetting one — say a stale `backdrop-filter: blur(14px)` on the outer in floating mode — yields an invisible blurred strip dragging across the viewport behind nothing.
+
+**4. Single timing curve across every property.** Eight properties on eight curves reads as eight animations. Same eight on `var(--dur-mid)` + `var(--ease-out)` reads as one motion. Use `cubic-bezier(0.16, 1, 0.3, 1)` (exponential ease-out) and ~520 ms.
+
+## The property morph (10 properties, one curve)
+
+| Element | Property | Default | Floating |
+|---|---|---|---|
+| `.nav` | `padding-block` | `0` | `var(--space-2xs)` |
+| `.nav` | `background-color` | dark/0.62 | `transparent` |
+| `.nav` | `border-block-end-color` | rule | `transparent` |
+| `.nav` | `backdrop-filter` | `saturate(1.4) blur(14px)` | `blur(0)` |
+| `.nav__inner` | `max-width` | `var(--page-max)` | `~58rem` |
+| `.nav__inner` | `min-height` | `60px` | `52px` |
+| `.nav__inner` | `padding-block` | `12px` | `4px` |
+| `.nav__inner` | `border-radius` | `0` | `var(--radius-pill, 999px)` |
+| `.nav__inner` | `background-color` | `transparent` | dark/0.82 |
+| `.nav__inner` | `backdrop-filter` | `blur(0)` | `blur(18px)` |
+| `.nav__inner` | `box-shadow` | `none` | drop + tinted glow + inset hairline |
+| `.nav__inner` | `transform` | `translateY(0)` | `translateY(12px)` |
+
+Use `blur(0)` not `none` — `none` snaps, `blur(0)` transitions. `backdrop-filter` transitions are 2024+ baseline (Chrome 107+, Safari 14+, Firefox 103+).
+
+## The scroll handler
+
+```js
+(() => {
+ const nav = document.querySelector(".nav");
+ if (!nav) return;
+ const THRESHOLD = 80; // ≥ 60 px to avoid micro-scroll twitches
+ let floating = false;
+ let ticking = false;
+ const update = () => {
+ const next = window.scrollY > THRESHOLD;
+ if (next !== floating) { // boolean-flip guard — toggle once per state change
+ floating = next;
+ nav.classList.toggle("is-floating", floating);
+ }
+ };
+ window.addEventListener("scroll", () => {
+ if (ticking) return;
+ ticking = true;
+ requestAnimationFrame(() => { update(); ticking = false; });
+ }, { passive: true }); // mobile scroll-perf — keep main thread free
+ update();
+})();
+```
+
+Three discipline points:
+- **`passive: true`** — listener won't `preventDefault`, browser keeps main thread free for scrolling.
+- **`requestAnimationFrame` throttle** — caps to 60 calls/s, aligns with paint.
+- **Boolean-flip guard** — class operation runs once per state change, not once per scroll event.
+
+## Anti-patterns Hallmark refuses
+
+1. Two separate `` elements that swap via opacity. Doubles DOM, fights focus order, can desync content.
+2. Animating `top` / `margin-top` to add the floating offset. Triggers layout. `transform` is the only correct lever.
+3. Using `backdrop-filter: none` in the floating state. Snaps. Use `blur(0)` so it transitions.
+4. Letting nav height change between states. Causes content jitter — the single most damaging mistake in this pattern.
+5. Different transition durations per property ("speed up the radius", "delay the shadow"). Reads as broken even though everything moves. One curve.
+6. `scroll` event without `{ passive: true }` or without rAF throttling. Both kill mobile scroll perf.
+7. Threshold = 0 (morph fires at the slightest scroll). Too jumpy. Use ≥ 60 px.
+8. Forgetting the boolean-flip guard — toggling the class on every scroll event causes layout thrash.
+
+The pattern works because it's restrained — one orchestrated morph, no embellishment. Pour the polish into the timing curve, the shadow stack, and the height-constant math; not into "more."