// s08 Ways in. Two option cards side by side from the cut: "Claude app" (full) and "Claude Code"
// (waiting at 45%). On "no install" a chip pins to the app card. On "Claude Code" the focus moves to
// the second card, and its loop builds on the narrator's words: render, look, fix, each node lighting
// as its arc draws, then the loop closes. Below, on "Start", an effort control (low / medium / high);
// hero move: on "medium" the accent pill springs onto medium (ζ 0.82). On "raise" a "raise" chip
// waits under high; on "after two" two red ✕ marks appear 120 ms apart and "raise" lights.
export default function build(root, k) {
const { make, place, show, cueIn, spring01, easeOut, easeInOut, p01, mix, check, sfx, box, overlap, C } = k;
const { cutIn, cutOut } = k.win;
const FPS = 60;
const tNoInstall = cueIn(k.id, "no install");
const tCC = cueIn(k.id, "claude code");
const tRender = cueIn(k.id, "renders");
const tLook = cueIn(k.id, "looks");
const tFix = cueIn(k.id, "fixes");
const tStart = cueIn(k.id, "start at");
const tMedium = cueIn(k.id, "medium");
const tRaise = cueIn(k.id, "raise");
const tMiss = cueIn(k.id, "after two");
// ---- the two cards ----
// A static layout: the cards sit high enough that the effort rule fits below them without a move
// (a rise before "Start" was tried; "itself" runs to 121.52 s, so the pause is too short).
const CY = 140, CH = 500;
const cardA = place(make("div", "card", "", root), 80, CY, 430, CH);
const cardB = place(make("div", "card", "", root), 550, CY, 450, CH);
make("div", "h2", "Claude app", cardA, { position: "absolute", left: "36px", top: "34px" });
make("div", "h2", "Claude Code", cardB, { position: "absolute", left: "36px", top: "34px" });
const noInstall = place(make("div", "chip accent", "no install", cardA, { fontSize: "30px" }), 36, 108);
// The app: a chat on the left, the preview panel on the right with the piece playing in it.
const icon = make("div", "", "", cardA, { position: "absolute", left: "36px", top: "236px" });
icon.innerHTML = ``;
// The loop: three nodes on a circle with arcs between them, drawn on as each node lights.
// Radius 130; each arc leaves its pill where the circle clears it (+6 px): 42° for the wide top
// pill, 24° for the side pills, whose edge the circle crosses steeply.
const LC = [225, 316], LR = 130;
const NODES = [["render", -90, tRender, 42], ["look", 30, tLook, 24], ["fix", 150, tFix, 24]];
const at = (deg, r = LR) => [LC[0] + r * Math.cos((deg * Math.PI) / 180), LC[1] + r * Math.sin((deg * Math.PI) / 180)];
const svgNS = "http://www.w3.org/2000/svg";
const loopSvg = document.createElementNS(svgNS, "svg");
loopSvg.setAttribute("width", 450); loopSvg.setAttribute("height", CH);
Object.assign(loopSvg.style, { position: "absolute", left: "0px", top: "0px", overflow: "visible" });
cardB.appendChild(loopSvg);
// Arc from node i to node i+1, clear of both pills, with an arrowhead at its end.
const arcs = NODES.map(([, a0, , g0], i) => {
const a1 = NODES[(i + 1) % 3][1] + (i === 2 ? 360 : 0), g1 = NODES[(i + 1) % 3][3];
const s = a0 + g0, e = a1 - g1;
const [x0, y0] = at(s), [x1, y1] = at(e);
const path = document.createElementNS(svgNS, "path");
path.setAttribute("d", `M${x0.toFixed(2)} ${y0.toFixed(2)} A${LR} ${LR} 0 0 1 ${x1.toFixed(2)} ${y1.toFixed(2)}`);
Object.entries({ fill: "none", stroke: C.ink, "stroke-width": 2.5, "stroke-linecap": "round" }).forEach(([k2, v]) => path.setAttribute(k2, v));
const head = document.createElementNS(svgNS, "path");
const tx = -Math.sin((e * Math.PI) / 180), ty = Math.cos((e * Math.PI) / 180); // tangent at the end
const nx = -ty, ny = tx;
head.setAttribute("d", `M${(x1 - 11 * tx + 7 * nx).toFixed(2)} ${(y1 - 11 * ty + 7 * ny).toFixed(2)} L${x1.toFixed(2)} ${y1.toFixed(2)} L${(x1 - 11 * tx - 7 * nx).toFixed(2)} ${(y1 - 11 * ty - 7 * ny).toFixed(2)}`);
Object.entries({ fill: "none", stroke: C.ink, "stroke-width": 2.5, "stroke-linecap": "round", "stroke-linejoin": "round" }).forEach(([k2, v]) => head.setAttribute(k2, v));
loopSvg.append(path, head);
return { path, head, len: 0 };
});
const pills = NODES.map(([name, deg]) => {
const [x, y] = at(deg);
const p = make("div", "", name, cardB, { position: "absolute", left: `${(x - 72).toFixed(2)}px`, top: `${(y - 29).toFixed(2)}px`, width: "144px", height: "58px", borderRadius: "29px",
display: "flex", alignItems: "center", justifyContent: "center", fontSize: "32px", fontWeight: "620", background: C.paper, boxShadow: `0 0 0 2.5px ${C.ink}` });
return p;
});
const tClose = tFix + 0.55; // fix → render closes the loop
const arcAt = [tLook - 0.25, tFix - 0.25, tClose]; // each arc draws just before the next node lights
// ---- effort ----
const EY = 720;
const effortLabel = place(make("div", "", "Effort", root, { fontSize: "30px", fontWeight: "560", color: C.muted }), 80, EY - 52);
const SEG_W = 920 / 3, SEG_H = 96;
const control = place(make("div", "", "", root, { background: C.paper, borderRadius: `${SEG_H / 2}px`, boxShadow: "0 1px 0 #d8d6d0 inset, 0 18px 50px rgba(28,28,26,0.10), 0 2px 6px rgba(28,28,26,0.06)" }), 80, EY, 920, SEG_H);
const sel = make("div", "", "", control, { position: "absolute", left: `${SEG_W + 8}px`, top: "8px", width: `${SEG_W - 16}px`, height: `${SEG_H - 16}px`, borderRadius: `${(SEG_H - 16) / 2}px`, background: C.accent });
sel.dataset.track = "s08-pill";
const segs = ["low", "medium", "high"].map((txt, i) => make("div", "", txt, control, { position: "absolute", left: `${i * SEG_W}px`, top: "0px", width: `${SEG_W}px`, height: `${SEG_H}px`,
display: "flex", alignItems: "center", justifyContent: "center", fontSize: "34px", fontWeight: "620", color: C.muted }));
const PILL = { duration: 0.7, damping: 0.82 };
// Under the control: two misses under "medium", then "raise" under "high".
const RY = EY + SEG_H + 40;
const miss = (cx) => {
const m = make("div", "", ``, root, { position: "absolute", left: `${cx - 32}px`, top: `${RY - 4}px`, width: "64px", height: "64px" });
return m;
};
const MID = 80 + 1.5 * SEG_W, HIGH = 80 + 2.5 * SEG_W;
const misses = [miss(MID - 40), miss(MID + 40)];
const arrow = make("div", "", ``, root,
{ position: "absolute", left: `${(MID + HIGH) / 2 - 32}px`, top: `${RY + 16}px` });
const raise = make("div", "", `raise`, root, {
position: "absolute", top: `${RY}px`, height: "56px", display: "flex", alignItems: "center", gap: "10px", padding: "0 26px 0 20px", borderRadius: "28px", fontSize: "32px", fontWeight: "620" });
const tMiss2 = tMiss + 0.12, tLit = tMiss2 + 0.25;
sfx(tMedium + 0.12 + 1 / FPS, "toggle"); // the pill lands on medium
sfx(tLit + 1 / FPS, "pop"); // raise lights
// ---- checks ----
// The pill's settle: within 0.5% of its size (ζ 0.82 scale spring).
let pillSettle = tMedium;
for (let n = 1; n < 120; n++) if (Math.abs(1 - spring01(tMedium + n / FPS, tMedium, PILL)) * 0.3 * 288 > 0.5) pillSettle = tMedium + (n + 1) / FPS;
check("s08 the pill settles on medium by f" + Math.round((pillSettle - tMedium) * FPS) + " after the cue", [pillSettle, cutOut - 1 / FPS], () => {
const p = box(sel), m = box(segs[1]);
return (Math.abs(p.cx - m.cx) < 0.5 && Math.abs(p.w - (SEG_W - 16)) < 0.5) || `pill cx ${p.cx.toFixed(2)} w ${p.w.toFixed(2)}`;
});
const litNow = () => pills.map((p) => p.style.background === "rgb(47, 85, 224)");
[[tLook - 0.05, 0], [tFix - 0.05, 1], [tStart - 0.05, 2]].forEach(([at0, i]) =>
check(`s08 the loop lights in order: only “${NODES[i][0]}” at ${at0.toFixed(2)} s`, at0, () => litNow().every((v, j) => v === (j === i)) || `lit ${litNow()}`));
check("s08 each arc ends clear of the pills", tStart, () => {
const ends = arcs.map((a) => { const L = a.path.getTotalLength(); return [a.path.getPointAtLength(0), a.path.getPointAtLength(L)]; }).flat();
const c = box(cardB);
const hit = ends.find((pt) => pills.some((p) => { const b = box(p); const x = pt.x + c.x, y = pt.y + c.y; return x > b.x - 4 && x < b.x + b.w + 4 && y > b.y - 4 && y < b.y + b.h + 4; }));
return !hit || `arc end at ${hit.x.toFixed(0)}, ${hit.y.toFixed(0)} under a pill`;
});
check("s08 raise lights only after both misses", [tMiss - 0.2, cutOut - 1 / FPS], (t) => {
const on = raise.style.color === "rgb(255, 255, 255)" || raise.style.color === "#ffffff";
const both = Number(misses[1].style.opacity) > 0.5;
return !on || both || "raise lit before the second miss";
});
check("s08 the misses are 120 ms apart", tMiss, () => Math.abs(tMiss2 - tMiss - 0.12) < 1e-9 || "stagger");
check("s08 the last change holds ≥ 1.5 s before the cut", cutOut - 1 / FPS, () => cutOut - tMiss >= 1.5 || `hold ${(cutOut - tMiss).toFixed(2)} s`);
check("s08 cards, control and marks never overlap", [cutIn, cutOut - 1 / FPS], () => {
const els = [cardA, cardB, effortLabel, control, ...misses, arrow, raise];
for (let i = 0; i < els.length; i++) for (let j = i + 1; j < els.length; j++) {
const o = overlap(els[i], els[j]);
if (o > 0) return `${i} x ${j}: ${o | 0} px²`;
}
return true;
});
check("s08 the loop's pills stay inside their card", [tFix, cutOut - 1 / FPS], () => {
const c = box(cardB);
return pills.every((p) => { const b = box(p); return b.x >= c.x + 16 && b.x + b.w <= c.x + c.w - 16 && b.y + b.h <= c.y + c.h - 16; }) || "a pill leaves the card";
});
check("s08 everything stays inside the stage", [cutIn, cutOut - 1 / FPS], () =>
[cardA, cardB, control, raise].every((el) => { const b = box(el); return b.x >= 0 && b.x + b.w <= 1080 && b.y >= 128 && b.y + b.h <= 1120; }) || "outside");
let measured = false;
return (t) => {
if (!measured) { arcs.forEach((a) => { a.len = a.path.getTotalLength(); a.path.style.strokeDasharray = `${a.len} ${a.len}`; }); measured = true; }
// Focus: the app card first; Claude Code from its cue; both step back for the effort rule.
const toCC = easeOut(p01(t, tCC, 0.5)), toEffort = easeOut(p01(t, tStart, 0.5));
cardA.style.opacity = (1 - 0.45 * toCC).toFixed(4);
cardB.style.opacity = (0.45 + 0.55 * toCC - 0.45 * toEffort).toFixed(4);
show(noInstall, t, tNoInstall, Infinity, { dur: 0.45, dx: -16, dy: 0 });
// The loop: each arc draws (ease-in-out, 0.4 s) and the next node lights; the current node is accent.
arcs.forEach((a, i) => {
const p = easeInOut(p01(t, arcAt[i], 0.4));
a.path.style.strokeDashoffset = (a.len * (1 - p)).toFixed(2);
a.path.style.opacity = p > 0 ? "1" : "0";
a.head.style.opacity = easeOut(p01(t, arcAt[i] + 0.3, 0.15)).toFixed(4);
});
pills.forEach((p, i) => {
const t0 = NODES[i][2], next = i < 2 ? NODES[i + 1][2] : Infinity;
const on = easeOut(p01(t, t0, 0.2)) * (1 - easeOut(p01(t, next, 0.2)));
p.style.background = on > 0.5 ? C.accent : C.paper;
p.style.color = on > 0.5 ? "#ffffff" : C.ink;
p.style.boxShadow = `0 0 0 2.5px ${on > 0.5 ? C.accent : C.ink}`;
// Each pill pops in on its word (scale spring ζ 0.82; opacity eases, never bounces).
p.style.transform = `scale(${(0.8 + 0.2 * spring01(t, t0, { duration: 0.5, damping: 0.82 })).toFixed(4)})`;
p.style.opacity = easeOut(p01(t, t0, 0.25)).toFixed(4);
p.style.visibility = t > t0 ? "visible" : "hidden";
});
// Effort: the control on "Start"; the pill pops onto medium on its cue (scale spring ζ 0.82).
show(effortLabel, t, tStart, Infinity, { dur: 0.45, dy: 10 });
show(control, t, tStart + 0.06, Infinity, { dur: 0.5, dy: 14 });
const s = spring01(t, tMedium, PILL);
sel.style.transform = `scale(${(0.7 + 0.3 * s).toFixed(4)})`;
sel.style.opacity = easeOut(p01(t, tMedium, 0.25)).toFixed(4);
segs[1].style.color = mix(C.muted, "#ffffff", easeOut(p01(t, tMedium + 0.05, 0.25)));
// Raise, then the two misses, then raise lights.
show(raise, t, tRaise, Infinity, { dur: 0.45, dy: 10 });
const lit = easeOut(p01(t, tLit, 0.3));
raise.style.left = `${HIGH - raise.offsetWidth / 2}px`;
raise.style.background = mix("#ffffff", C.accent, lit);
raise.style.color = lit > 0.5 ? "#ffffff" : C.muted;
raise.style.boxShadow = lit > 0.5 ? "none" : `0 0 0 2px ${C.line}`;
show(misses[0], t, tMiss, Infinity, { dur: 0.35, dy: 0, scale: 0.8 });
show(misses[1], t, tMiss2, Infinity, { dur: 0.35, dy: 0, scale: 0.8 });
show(arrow, t, tMiss2 + 0.1, Infinity, { dur: 0.35, dx: -10, dy: 0 });
};
}