Website: the story phone comes apart as its eighth chapter

The separate Under the glass section is gone; its exploded phone is now
chapter 8 of How it works, so the pinned story phone carries straight
into it. After Out of sight the list closes, the drawn phone is swapped
for its layered double (the same home screen closed; measured at about
1-2% of pixels apart, from anti-aliasing), which tilts and comes apart,
holds, and goes back together before the section ends. The chapter is
two screens long and its text stays put meanwhile.

Labels redone: a column of numbered solid tags beside the stack, evenly
spaced, each joined by an elbow line to a dot on its layer's edge; the
stack moves left as it opens to make room. The glass sheen and the frame
now match the drawn phone, so the swap is not seen.

Also fixed: the layered phone sat at its full 414 px until first used,
which made the page wider than a phone and zoomed it out; it is scaled
from the start, and on phones the story stage clips. The story now
counts its progress in chapter heights rather than window heights,
which drifted apart on phones when the address bar hides. The long
chapter's height lost to the general chapter rule; it now wins.

Section numbers close up (Details 02 to Get it 06); the page alphabet's
U leads to the new chapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
TMCandClaude Opus 5.5 committed 2026-11-12 22:11:53 +01:00
1 parent 6c68cb1369
commit 058fecd27f
4 files changed
+205 -110

No files matched your search

+4 -2
View File
@@ -35,7 +35,7 @@ and open <http://localhost:8080>.
## The phone
The phone is drawn in HTML at 390 x 844 and scaled to fit, four times over: a looping teaser in
the hero, one driven by the scroll position through the seven "How it works" chapters
the hero, one driven by the scroll position through the eight "How it works" chapters
(`storyState()` in `script.js`), one in the playground that answers to touch and mouse, and the
list phone of the race (the playground's controller, locked to the app list). All of them render
the same state object, described in `blank()`. The 40 app names and icons on it are generic
@@ -49,7 +49,9 @@ Other moving parts, all in `script.js`:
wave, drag to jump between sections, tap to scroll there. On phones it shows while scrolling.
- **Detail-card switches** (`setupLook`): left-handed alphabet, comfy rows and icon shape, applied
to every phone on the page.
- **Under the glass** (`setupAnatomy`): the phone pulled apart into its layers as the section scrolls.
- **Every layer, pulled apart** (`setupAnatomy`): the story's eighth chapter. The story phone is
swapped for a double built from separate layers (it looks the same closed), which then comes
apart in 3D with a labelled tag per layer and goes back together.
- **Race** (`setupRace`): find an app in a shuffled grid of 40, then in the list with the alphabet.
- **Magnetic buttons and the finger cursor** (`setupPointerFx`), desktop only.
- **Easter eggs**: scrub to F in the playground, or type "flexera" anywhere.
+33 -35
View File
@@ -169,16 +169,27 @@
<div class="story__body">
<div class="story__stage" aria-hidden="true">
<div class="story__num" data-story-num>01</div>
<div class="phone-host" data-phone="story"></div>
<div class="story__phones">
<div class="phone-host" data-phone="story"></div>
<div class="xhost" data-xphone></div>
</div>
<svg class="anatomy__lines" data-xlines></svg>
<ol class="anatomy__labels" data-xlabels>
<li><i>05</i><b>Glass</b><span>No feed. No ads.</span></li>
<li><i>04</i><b>App list</b><span>A to Z + wave alphabet</span></li>
<li><i>03</i><b>Icons &amp; folders</b><span>Blur behind folders</span></li>
<li><i>02</i><b>Widgets</b><span>Clock, weather, pictures</span></li>
<li><i>01</i><b>Wallpaper</b><span>Yours. Untouched.</span></li>
</ol>
<ol class="story__rail" data-story-rail>
<li></li><li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li>
</ol>
</div>
<div class="story__steps">
<article class="step" data-step>
<div class="step__card">
<p class="step__no">01 / 07</p>
<p class="step__no">01 / 08</p>
<h3 class="step__title">Home, minus the noise.</h3>
<p class="step__text">Pages, folders and widgets, in the Android layout you already know. No feed, no ads, nothing you didn't put there yourself.</p>
<p class="step__gesture"><span>Gesture</span> None. Just look.</p>
@@ -186,7 +197,7 @@
</article>
<article class="step" data-step>
<div class="step__card">
<p class="step__no">02 / 07</p>
<p class="step__no">02 / 08</p>
<h3 class="step__title">Swipe up. A&nbsp;to&nbsp;Z.</h3>
<p class="step__text">Every app in one clean column, straight over your wallpaper. New messages show up under the app's name, so you see them before you open anything.</p>
<p class="step__gesture"><span>Gesture</span> Swipe up</p>
@@ -194,7 +205,7 @@
</article>
<article class="step" data-step>
<div class="step__card">
<p class="step__no">03 / 07</p>
<p class="step__no">03 / 08</p>
<h3 class="step__title">Ride the wave.</h3>
<p class="step__text">Slide your thumb along the letters on the edge. They swell around it like a wave, and the list drops that letter into the middle of the screen, right where your thumb already is. Swipe up and keep holding to scrub straight from home.</p>
<p class="step__gesture"><span>Gesture</span> Slide on the edge</p>
@@ -202,7 +213,7 @@
</article>
<article class="step" data-step>
<div class="step__card">
<p class="step__no">04 / 07</p>
<p class="step__no">04 / 08</p>
<h3 class="step__title">Folders in focus.</h3>
<p class="step__text">Open a folder and the rest of the screen melts into a soft blur, wallpaper included. Just the apps you came for.</p>
<p class="step__gesture"><span>Gesture</span> Tap</p>
@@ -210,7 +221,7 @@
</article>
<article class="step" data-step>
<div class="step__card">
<p class="step__no">05 / 07</p>
<p class="step__no">05 / 08</p>
<h3 class="step__title">Swipe into nothing.</h3>
<p class="step__text">There's no page left of your first one. Swipe there anyway and your camera opens. Or any app, or any shortcut you pick. Works past your last page too.</p>
<p class="step__gesture"><span>Gesture</span> Swipe past the edge</p>
@@ -218,7 +229,7 @@
</article>
<article class="step" data-step>
<div class="step__card">
<p class="step__no">06 / 07</p>
<p class="step__no">06 / 08</p>
<h3 class="step__title">Widgets that fit in.</h3>
<p class="step__text">A clock that lines up with your icons: the date, a one-line forecast, left, center or right, thin to bold. Next to it, a picture frame in your icon shape that turns to the next photo every ten seconds.</p>
<p class="step__gesture"><span>Gesture</span> Hold, then edit</p>
@@ -226,41 +237,28 @@
</article>
<article class="step" data-step>
<div class="step__card">
<p class="step__no">07 / 07</p>
<p class="step__no">07 / 08</p>
<h3 class="step__title">Out of sight.</h3>
<p class="step__text">Hold any app in the list and tap Hide app. Gone from the list and from search. Changed your mind? Undo, or bring it back from Settings whenever you like.</p>
<p class="step__gesture"><span>Gesture</span> Hold, then hide</p>
</div>
</article>
<article class="step step--long" data-step id="anatomy">
<div class="step__card">
<p class="step__no">08 / 08</p>
<h3 class="step__title">Every layer. <span class="grad">Pulled apart.</span></h3>
<p class="step__text">Under the glass sit five layers: your wallpaper, the widgets, icons and folders, the app list with its wave alphabet, and the glass on top. No feed and no ads hiding in between.</p>
<p class="step__gesture"><span>Gesture</span> Keep scrolling</p>
</div>
</article>
</div>
</div>
</section>
<!-- UNDER THE GLASS: the phone pulled apart into its layers -->
<section class="anatomy" id="anatomy" aria-labelledby="anatomy-title">
<div class="anatomy__sticky">
<header class="anatomy__head">
<p class="tag rv">[ 02 ] Under the glass</p>
<h2 class="h2 rv" id="anatomy-title">
<span class="line"><span class="line__in">Every layer.</span></span>
<span class="line"><span class="line__in grad">Pulled apart.</span></span>
</h2>
</header>
<div class="anatomy__stage" aria-hidden="true"><div class="xhost" data-xphone></div></div>
<ol class="anatomy__labels" data-xlabels>
<li><b>Glass</b><span>No feed. No ads.</span></li>
<li><b>App list</b><span>A to Z, wave alphabet on the edge.</span></li>
<li><b>Icons &amp; folders</b><span>Blur behind every open folder.</span></li>
<li><b>Widgets</b><span>Clock, weather, pictures.</span></li>
<li><b>Wallpaper</b><span>Yours. Untouched.</span></li>
</ol>
</div>
</section>
<!-- DETAILS -->
<section class="details" id="details" aria-labelledby="details-title">
<header class="section-head">
<p class="tag rv">[ 03 ] The details</p>
<p class="tag rv">[ 02 ] The details</p>
<h2 class="h2 rv" id="details-title">
<span class="line"><span class="line__in">Small things.</span></span>
<span class="line"><span class="line__in grad">Done properly.</span></span>
@@ -327,7 +325,7 @@
<!-- PRIVACY -->
<section class="privacy" id="privacy" aria-labelledby="privacy-title">
<p class="tag tag--ink rv">[ 04 ] Privacy</p>
<p class="tag tag--ink rv">[ 03 ] Privacy</p>
<h2 class="privacy__title rv" id="privacy-title">It goes online for <span class="privacy__hl">exactly two</span> things.</h2>
<div class="privacy__list">
<div class="pitem rv">
@@ -348,7 +346,7 @@
<!-- PLAYGROUND -->
<section class="play" id="play" aria-labelledby="play-title">
<header class="section-head">
<p class="tag rv">[ 05 ] Playground</p>
<p class="tag rv">[ 04 ] Playground</p>
<h2 class="h2 rv" id="play-title">
<span class="line"><span class="line__in">Go on.</span></span>
<span class="line"><span class="line__in grad">Touch it.</span></span>
@@ -385,7 +383,7 @@
<!-- RACE -->
<section class="race" id="race" aria-labelledby="race-title">
<header class="section-head">
<p class="tag rv">[ 06 ] Race</p>
<p class="tag rv">[ 05 ] Race</p>
<h2 class="h2 rv" id="race-title">
<span class="line"><span class="line__in">Find it.</span></span>
<span class="line"><span class="line__in grad">Faster.</span></span>
@@ -415,7 +413,7 @@
<!-- GET IT -->
<section class="get" id="get" aria-labelledby="get-title">
<p class="tag tag--ink rv">[ 07 ] Get it</p>
<p class="tag tag--ink rv">[ 06 ] Get it</p>
<h2 class="get__title rv" id="get-title">
<span class="line"><span class="line__in">Get</span></span>
<span class="line"><span class="line__in">Flexera.</span></span>
+131 -40
View File
@@ -123,6 +123,8 @@
const MENU_X = 120;
const CLOCK_W = 4 * CW - 36;
const DEFAULT_WEIGHT = 300;
/** The story runs from 0 to 9: chapters 1 to 7 take one unit each, chapter 8 two. */
const STORY_END = 9;
/** Icon shapes, as border radii for icons and for the picture widget. */
const SHAPES = {
squircle: { icon: '30%', widget: '30px' },
@@ -525,7 +527,17 @@
*/
function storyState(ph, P) {
const s = blank();
P = clamp(P, 0, 7);
P = clamp(P, 0, STORY_END);
if (P >= 7) {
// Chapter 8, two units long: the list closes and the phone is home again. From there on the
// layered phone (setupAnatomy) takes its place and comes apart; this one is hidden.
const t = (P - 7) / 2;
s.drawer = 1 - E(t, 0, 0.1);
s.list = ph.scrollFor(L('P'));
s.hidden = { Podcasts: 1 };
s.snack = 1 - E(t, 0, 0.08);
return s;
}
const ch = Math.min(6, Math.floor(P));
const t = P - ch;
const F = s.finger;
@@ -636,7 +648,11 @@
return s;
}
function setupStory(ph) {
/** Chapter 8, from its start (0) to its end (1): when the phones swap, and how far apart the layers are. */
const SWAP_AT = 0.12;
const layersApart = (t) => smooth(seg(t, 0.14, 0.42)) * (1 - smooth(seg(t, 0.74, 0.94)));
function setupStory(ph, layered) {
const box = document.querySelector('.story__steps');
const steps = [...document.querySelectorAll('[data-step]')];
const num = document.querySelector('[data-story-num]');
@@ -644,14 +660,22 @@
let lastChapter = -1;
let lastP = -1;
return function update(force) {
// In chapters, not window heights: on phones a chapter is the small viewport height, while
// the window grows when the browser's address bar slides away.
const vh = window.innerHeight;
const P = clamp(-box.getBoundingClientRect().top / vh + 0.5, 0, 7);
const ch = Math.min(6, Math.floor(P));
// Without motion, each chapter shows its finished picture.
const shown = reduced ? ch + 0.985 : P;
const unit = steps[0].offsetHeight || vh;
const P = clamp((-box.getBoundingClientRect().top + vh / 2) / unit, 0, STORY_END);
const ch = P >= 7 ? 7 : Math.floor(P);
// Without motion, each chapter shows its finished picture (chapter 8: pulled apart).
const shown = reduced ? (ch === 7 ? 7.98 : ch + 0.985) : P;
if (force || Math.abs(shown - lastP) > 0.0002) {
lastP = shown;
ph.render(storyState(ph, shown));
if (layered) {
const t = shown >= 7 ? (shown - 7) / 2 : 0;
const swapped = shown >= 7 && t >= SWAP_AT;
layered.set(swapped ? layersApart(t) : 0, swapped, force);
}
}
if (ch !== lastChapter) {
lastChapter = ch;
@@ -1294,6 +1318,10 @@
el.style.setProperty('--shape', shape.icon);
el.style.setProperty('--shape-w', shape.widget);
});
document.querySelectorAll('.xphone').forEach((el) => {
el.classList.toggle('is-lefty', look.lefty);
el.classList.toggle('is-comfy', look.comfy);
});
rerender();
sync();
toast(message);
@@ -1556,35 +1584,48 @@
</div>`;
}
function setupAnatomy() {
const sec = document.querySelector('.anatomy');
/**
* The story phone's double, built from separate layers so that it can come apart in 3D (the
* drawn phone clips its contents and cannot). It sits exactly over the story phone; closed, it
* shows the same home screen, so swapping the two is not seen.
*/
function setupAnatomy(storyPhone) {
const host = document.querySelector('[data-xphone]');
if (!sec || !host) return null;
if (!host || !storyPhone) return null;
host.innerHTML = xphoneMarkup();
const xp = host.querySelector('.xphone');
const layers = [...xp.querySelectorAll('.xl')];
// Labels from the top: glass, list, icons, widgets, wallpaper.
const pins = [5, 4, 3, 2, 1].map((i) => layers[i].querySelector('.xl__pin'));
const labels = [...document.querySelectorAll('[data-xlabels] li')];
const sticky = sec.querySelector('.anatomy__sticky');
const stage = host.closest('.story__stage');
const rail = stage.querySelector('[data-story-rail]');
const pics = [...xp.querySelectorAll('.pic')];
const svg = stage.querySelector('[data-xlines]');
const NS = 'http://www.w3.org/2000/svg';
const lines = labels.map(() => {
const path = document.createElementNS(NS, 'path');
const dot = document.createElementNS(NS, 'circle');
dot.setAttribute('r', '4.5');
svg.append(path, dot);
return { path, dot };
});
let scale = 1;
const fit = () => { scale = host.clientWidth / 414; };
fit();
if ('ResizeObserver' in window) new ResizeObserver(fit).observe(host);
let lastE = -1;
return function update(force) {
const rc = sec.getBoundingClientRect();
const vh = window.innerHeight;
if (!force && (rc.bottom < -50 || rc.top > vh + 50)) return;
const p = clamp(-rc.top / Math.max(1, rc.height - vh));
let e = smooth(seg(p, 0.08, 0.4)) * (1 - smooth(seg(p, 0.8, 0.98)));
if (reduced) e = p > 0.04 && p < 0.96 ? 1 : 0;
if (!force && Math.abs(e - lastE) < 0.0005) return;
lastE = e;
let swapped = null;
/** Same picture as the story phone shows, so the swap is not seen. */
function syncPictures() {
pics.forEach((p, i) => p.classList.toggle('on', i === storyPhone.picIndex));
}
/** Puts the layers where e (0 closed, 1 fully apart) wants them. */
function arrange(e) {
const GAP = 150; // between layers, in the phone's pixels
const tilt = 56 * e;
const sc = scale * (1 - 0.28 * e);
const ox = 207 * (scale - sc);
const sc = scale * (1 - 0.36 * e);
const wide = window.innerWidth > 900;
// Opening, the stack moves left to leave room for its labels.
const shift = wide ? -e * Math.min(140, stage.clientWidth * 0.17) : 0;
const ox = 207 * (scale - sc) + shift;
// The layers rise as they separate; lower the whole stack by half of that, to keep it centred.
const oy = 434 * (scale - sc) + e * 2.5 * GAP * Math.sin((tilt * Math.PI) / 180) * sc;
xp.style.setProperty('--e', e.toFixed(3));
@@ -1594,19 +1635,66 @@
});
// Flat, it is just a home screen: the app list layer only shows once the stack opens.
layers[4].style.opacity = smooth(seg(e, 0.15, 0.6)).toFixed(3);
const wide = window.innerWidth > 900;
const base = sticky.getBoundingClientRect();
let prevY = -Infinity;
labels.forEach((li, k) => {
li.style.opacity = seg(e, 0.55, 0.95).toFixed(3);
if (!wide) return;
const pr = pins[k].getBoundingClientRect();
const x = Math.min(pr.left - base.left + 16, base.width - 250);
const y = Math.max(pr.top - base.top - 12, prevY + 60); // never on top of the one above
prevY = y;
li.style.transform = `translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
}
/** The labels: one column beside the stack, evenly spaced, each joined to its layer's edge. */
function label(e) {
const shown = seg(e, 0.55, 0.95);
svg.style.opacity = shown.toFixed(3);
labels.forEach((li) => { li.style.opacity = shown.toFixed(3); });
if (window.innerWidth <= 900 || shown <= 0) return;
const base = stage.getBoundingClientRect();
let right = -Infinity;
for (const l of layers) right = Math.max(right, l.getBoundingClientRect().right);
const LABEL_W = 214;
const colX = clamp(right - base.left + 34, 0, base.width - LABEL_W - 8);
const pts = pins.map((pin) => {
const r = pin.getBoundingClientRect();
return { x: r.left - base.left, y: r.top - base.top };
});
const ys = [];
let prev = -Infinity;
for (const pt of pts) {
const y = Math.max(pt.y, prev + 76); // never closer than one tag's height
ys.push(y);
prev = y;
}
const spill = ys[ys.length - 1] + 44 - base.height;
if (spill > 0) for (let k = 0; k < ys.length; k++) ys[k] -= spill;
labels.forEach((li, k) => {
li.style.transform = `translate(${colX.toFixed(1)}px, ${ys[k].toFixed(1)}px)`;
const { x, y } = pts[k];
const bend = Math.max(x + 16, colX - 26);
lines[k].path.setAttribute('d', `M${x.toFixed(1)} ${y.toFixed(1)} L${bend.toFixed(1)} ${ys[k].toFixed(1)} L${colX.toFixed(1)} ${ys[k].toFixed(1)}`);
lines[k].dot.setAttribute('cx', x.toFixed(1));
lines[k].dot.setAttribute('cy', y.toFixed(1));
});
}
const fit = () => {
scale = host.clientWidth / 414;
arrange(Math.max(0, lastE)); // scaled from the start: at full size it would widen the page
};
fit();
if ('ResizeObserver' in window) new ResizeObserver(fit).observe(host);
function set(e, on, force) {
if (on !== swapped) {
swapped = on;
host.style.visibility = on ? 'visible' : 'hidden';
storyPhone.host.style.visibility = on ? 'hidden' : '';
if (on) syncPictures();
lastE = -1;
}
if (!on) {
label(0);
rail.style.opacity = '';
return;
}
if (!force && Math.abs(e - lastE) < 0.0005) return;
lastE = e;
arrange(e);
rail.style.opacity = (1 - e).toFixed(3);
label(e);
}
return { set, syncPictures, element: xp };
}
/* ------------------------------------------------------------------ the race */
@@ -1816,11 +1904,11 @@
document.querySelectorAll('.xl [data-date]').forEach((el) => { el.textContent = date; });
}
const story = phones.story ? setupStory(phones.story) : null;
const layered = setupAnatomy(phones.story);
const story = phones.story ? setupStory(phones.story, layered) : null;
const play = phones.play
? setupPlay(phones.play, { caption: document.querySelector('[data-caption]'), demos: document.querySelector('.play'), egg: true })
: null;
const anatomy = setupAnatomy();
const race = phones.race ? setupRace(phones.race) : null;
tick();
setInterval(tick, 10000);
@@ -1829,7 +1917,6 @@
if (story) story(true);
if (play) play.render();
if (race) race.render();
if (anatomy) anatomy(true);
};
setupLook(phones, rerender);
const pageWave = setupPageWave();
@@ -1838,7 +1925,12 @@
setupEggs();
// Picture widgets turn to the next picture now and then.
if (!reduced) setInterval(() => Object.values(phones).forEach((p) => p.nextPicture()), 3800);
if (!reduced) {
setInterval(() => {
Object.values(phones).forEach((p) => p.nextPicture());
if (layered) layered.syncPictures();
}, 3800);
}
// Fonts change nothing that is measured, but measure again once they are in, to be sure.
if (document.fonts && document.fonts.ready) {
@@ -1882,7 +1974,6 @@
}
}
if (story) story();
if (anatomy) anatomy();
pageWave();
}
+37 -33
View File
@@ -289,7 +289,12 @@ section { position: relative; }
color: rgba(245, 244, 255, .045); /* filled, not outlined, for the same reason as .hero__ghost */
pointer-events: none; user-select: none;
}
.story .phone-host { height: min(84vh, 880px); height: min(84svh, 880px); position: relative; z-index: 1; }
.story__phones { position: relative; z-index: 1; height: min(84vh, 880px); height: min(84svh, 880px); aspect-ratio: 414 / 868; }
.story__phones .phone-host, .story__phones .xhost { position: absolute; inset: 0; height: auto; }
.story__phones .xhost { visibility: hidden; }
/* Chapter 8 is two screens long; its text stays put while the phone comes apart. */
.step.step--long { min-height: 200vh; min-height: 200svh; align-items: flex-start; }
.step.step--long .step__card { position: sticky; top: max(96px, calc(50vh - 170px)); }
.story__rail { position: absolute; right: 0; top: 50%; translate: 0 -50%; display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; list-style: none; }
.story__rail li { width: 12px; height: 34px; border: var(--b) solid var(--fg); transition: background .3s, height .3s; }
.story__rail li.done { background: var(--fg); }
@@ -655,12 +660,12 @@ section { position: relative; }
.row__p { margin-top: 3px; font: 400 13px/1.2 var(--ui); color: #cfcbef; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row.flash { background: rgba(255, 255, 255, .12); }
/* Settings from the detail cards. */
.screen.is-comfy .row { height: 66px; }
.screen.is-comfy .row--note { height: 78px; }
.screen.is-comfy .row .ic { width: 48px; height: 48px; }
.screen.is-comfy .row__n { font-size: 19.5px; }
.screen.is-lefty .list { padding: 0 20px 0 58px; }
.screen.is-lefty .alpha { right: auto; left: 8px; }
.is-comfy .row { height: 66px; }
.is-comfy .row--note { height: 78px; }
.is-comfy .row .ic { width: 48px; height: 48px; }
.is-comfy .row__n { font-size: 19.5px; }
.is-lefty .list { padding: 0 20px 0 58px; }
.is-lefty .alpha { right: auto; left: 8px; }
.row.held { background: rgba(255, 255, 255, .1); }
.alpha { position: absolute; right: 8px; bottom: 112px; width: 30px; display: flex; flex-direction: column; align-items: center; z-index: 2; }
@@ -767,30 +772,34 @@ section { position: relative; }
.over-phone .phone-host, .over-phone .phone-host * { cursor: none !important; }
.btn, .chip { transition: transform .14s ease, box-shadow .14s ease, background .2s, color .2s, translate .35s cubic-bezier(.2, .8, .2, 1); }
/* ---------- under the glass: the phone pulled apart ---------- */
.anatomy { position: relative; height: 320vh; height: 320svh; }
.anatomy__sticky {
position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden;
display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); align-items: center;
max-width: var(--max); margin: 0 auto; padding: 70px var(--pad) 30px;
}
.anatomy__stage { position: relative; display: grid; place-items: center; height: 100%; }
.xhost { position: relative; height: min(68vh, 720px); height: min(68svh, 720px); aspect-ratio: 414 / 868; perspective: 2600px; }
/* ---------- chapter 8: the phone pulled apart ---------- */
.xhost { perspective: 2600px; }
.xphone { position: absolute; left: 0; top: 0; width: 414px; height: 868px; transform-origin: 0 0; transform-style: preserve-3d; --e: 0; }
.xl { position: absolute; left: 12px; top: 12px; width: 390px; height: 844px; border-radius: 48px; overflow: hidden; font-family: var(--ui); color: #fff; box-shadow: 0 0 0 2px rgba(118, 250, 252, calc(var(--e) * .55)); }
.xl * { box-sizing: border-box; }
.xl .ic svg, .xl .gl { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.xl--body { left: 0; top: 0; width: 414px; height: 868px; border-radius: 60px; background: linear-gradient(150deg, #34324d, #121122 45%, #08070f); box-shadow: inset 0 0 0 2px #4a4767, 0 50px 90px -30px rgba(101, 61, 240, .7); }
.xl--body::before, .xl--body::after { content: ""; position: absolute; right: -4px; width: 4px; border-radius: 0 3px 3px 0; background: #2b2940; }
.xl--body::before { top: 190px; height: 70px; }
.xl--body::after { top: 290px; height: 110px; }
.xl--body { overflow: visible; left: 0; top: 0; width: 414px; height: 868px; border-radius: 60px; background: linear-gradient(150deg, #34324d, #121122 45%, #08070f); box-shadow: inset 0 0 0 2px #4a4767, inset 0 0 0 5px #0a0914, 0 50px 90px -30px rgba(101, 61, 240, .7), 0 0 0 2px #000; }
.xl--list { background: rgba(5, 4, 20, calc(.35 + var(--e) * .3)); }
.xl--list .list { top: 46px; }
.xl--list .alpha { bottom: 112px; }
.xl--glass { background: linear-gradient(125deg, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0) 38%, rgba(255, 255, 255, .07) 68%, rgba(255, 255, 255, 0)); }
.xl__pin { position: absolute; right: 0; top: 46%; width: 1px; height: 1px; }
.xl--glass::before { content: ""; position: absolute; inset: 0; opacity: var(--e); background: linear-gradient(125deg, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0) 38%, rgba(255, 255, 255, .07) 68%, rgba(255, 255, 255, 0)); }
.xl__pin { position: absolute; right: 0; top: 30%; width: 1px; height: 1px; }
.anatomy__lines { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; z-index: 2; opacity: 0; }
.anatomy__lines path { fill: none; stroke: var(--cyan); stroke-width: 2; }
.anatomy__lines circle { fill: var(--bg); stroke: var(--cyan); stroke-width: 2.5; }
.anatomy__labels { list-style: none; margin: 0; padding: 0; }
.anatomy__labels li { position: absolute; left: 0; top: 0; width: 240px; padding-left: 66px; opacity: 0; pointer-events: none; will-change: transform; }
.anatomy__labels li::before { content: ""; position: absolute; left: 0; top: .55em; width: 56px; height: 2px; background: var(--cyan); }
.anatomy__labels b { display: block; font: 900 17px/1.1 var(--display); text-transform: uppercase; letter-spacing: -.02em; }
.anatomy__labels span { display: block; margin-top: 5px; font: 500 14px/1.35 var(--body); color: var(--dim); }
.anatomy__labels li {
position: absolute; left: 0; top: 0; z-index: 3; width: 214px; translate: 0 -50%;
padding: 9px 12px 10px; background: rgba(7, 7, 13, .94);
border: var(--b) solid var(--fg); box-shadow: 4px 4px 0 var(--violet);
opacity: 0; pointer-events: none; will-change: transform;
}
.anatomy__labels i { margin-right: 8px; font: 700 11px/1 var(--mono); font-style: normal; color: var(--cyan); }
.anatomy__labels b { font: 900 13px/1.15 var(--display); text-transform: uppercase; letter-spacing: -.01em; }
.anatomy__labels span { display: block; margin-top: 6px; font: 500 12px/1.3 var(--mono); color: var(--dim); white-space: nowrap; }
/* ---------- race ---------- */
.race__grid {
@@ -856,8 +865,10 @@ section { position: relative; }
.spinner { width: 100px; height: 100px; left: 2%; }
.story__body { display: block; padding: 0; }
.story__stage { position: sticky; top: 0; place-items: start center; padding-top: 70px; }
.story .phone-host { height: 52vh; height: 52svh; }
.story__stage { position: sticky; top: 0; place-items: start center; padding-top: 70px; overflow: hidden; } /* nothing the phones do may widen the page */
.story__phones { height: 52vh; height: 52svh; }
.step.step--long { align-items: flex-end; }
.step.step--long .step__card { position: sticky; top: auto; bottom: 4svh; }
.story__num { top: 32%; font-size: 58vw; }
.story__rail { right: 14px; top: 32%; gap: 6px; }
.story__rail li { width: 8px; height: 20px; }
@@ -892,14 +903,7 @@ section { position: relative; }
.pagewave a { height: 25px; }
.pagewave__bubble { right: 58px; }
.anatomy { height: 260svh; }
.anatomy__sticky { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; padding-top: 74px; gap: 8px; }
.anatomy__head .h2 { font-size: clamp(32px, 9vw, 48px); }
.xhost { height: 44svh; }
.anatomy__labels { display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 6px; }
.anatomy__labels li { position: static; width: auto; padding: 7px 10px; border: var(--b) solid var(--fg); transform: none !important; font-size: 12px; }
.anatomy__labels li::before, .anatomy__labels span { display: none; }
.anatomy__labels b { font-size: 11px; }
.anatomy__labels, .anatomy__lines { display: none; } /* the chapter's text names the layers */
.race__grid { grid-template-columns: minmax(0, 1fr); }
.race__side { order: -1; }