export const AZIMUTH = 45; export const ELEVATION = 30; export const SHADES = 4; const RADIANS = Math.PI / 180; const UNIT = Math.sqrt(1.6); export function cameraOf({ scale, azimuth = AZIMUTH, elevation = ELEVATION }) { const a = azimuth * RADIANS; const e = elevation * RADIANS; return { sinA: Math.sin(a), cosA: Math.cos(a), sinE: Math.sin(e), cosE: Math.cos(e), k: scale * UNIT }; } export function iso([x, y, z], projection) { const { origin } = projection; const { sinA, cosA, sinE, cosE, k } = cameraOf(projection); return [origin[0] + (x * sinA - y * cosA) * k, origin[1] + ((x * cosA + y * sinA) * sinE - z * cosE) * k]; } export function towardViewer(projection) { const { sinA, cosA } = cameraOf(projection); return [cosA, sinA]; } export function depthOf([x, y, z], projection) { const { sinA, cosA, sinE, cosE } = cameraOf(projection); return (x * cosA + y * sinA) * cosE + z * sinE; } const tenth = (value) => { const text = (Math.round(value * 10) / 10).toFixed(1).replace(/\.0$/, ""); return text === "-0" ? "0" : text; }; export function pathOf(points, closed = false) { if (!points.length) return ""; return points.map(([x, y], at) => `${at ? "L" : "M"}${tenth(x)} ${tenth(y)}`).join("") + (closed ? "Z" : ""); } export const joinPaths = (paths) => paths.filter(Boolean).join(""); export function roundedPlan({ x, y, w, d, r }, steps = 8) { const radius = Math.max(0, Math.min(r, w / 2, d / 2)); const corners = [ [x + w - radius, y + radius, -90], [x + w - radius, y + d - radius, 0], [x + radius, y + d - radius, 90], [x + radius, y + radius, 180], ]; const out = []; for (const [cx, cy, start] of corners) { const count = radius > 0 ? steps : 0; for (let step = 0; step <= count; step++) { const angle = (start + (count ? (90 * step) / count : 45)) * RADIANS; out.push([cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]); } } return out; } function signedArea(points) { let area = 0; for (let index = 0; index < points.length; index++) { const [ax, ay] = points[index]; const [bx, by] = points[(index + 1) % points.length]; area += ax * by - bx * ay; } return area / 2; } const oriented = (points) => (signedArea(points) < 0 ? [...points].reverse() : points); function convexHull(points) { const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]); if (sorted.length < 3) return sorted; const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); const lower = []; for (const point of sorted) { while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], point) <= 0) lower.pop(); lower.push(point); } const upper = []; for (let index = sorted.length - 1; index >= 0; index--) { const point = sorted[index]; while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], point) <= 0) upper.pop(); upper.push(point); } return [...lower.slice(0, -1), ...upper.slice(0, -1)]; } function runsOf(flags, want) { const count = flags.length; const runs = []; const start = flags.findIndex((flag, index) => flag !== flags[(index + count - 1) % count]); if (start < 0) return flags[0] === want ? [Array.from({ length: count + 1 }, (_, index) => index % count)] : []; let run = null; for (let step = 0; step < count; step++) { const index = (start + step) % count; if (flags[index] === want) { if (!run) run = [index]; run.push((index + 1) % count); } else if (run) { runs.push(run); run = null; } } if (run) runs.push(run); return runs; } export function extrude(ring, z, height, projection, options = {}) { const convex = options.convex ?? true; const count = ring.length; const counter = signedArea(ring) > 0; const [vx, vy] = towardViewer(projection); const { sinA, cosA } = cameraOf(projection); const normals = ring.map(([ax, ay], index) => { const [bx, by] = ring[(index + 1) % count]; const dx = bx - ax; const dy = by - ay; const length = Math.hypot(dx, dy) || 1; return (counter ? [dy / length, -dx / length] : [-dy / length, dx / length]); }); const facing = normals.map(([nx, ny]) => nx * vx + ny * vy > 1e-9); const top = ring.map(([x, y]) => iso([x, y, z + height], projection)); const bottom = ring.map(([x, y]) => iso([x, y, z], projection)); const outline = []; const crease = []; for (const run of runsOf(facing, false)) outline.push(pathOf(run.map((index) => top[index]))); for (const run of runsOf(facing, true)) { crease.push(pathOf(run.map((index) => top[index]))); if (height > 0) outline.push(pathOf(run.map((index) => bottom[index]))); } if (height > 0) { for (let index = 0; index < count; index++) { if (facing[(index + count - 1) % count] !== facing[index]) outline.push(pathOf([top[index], bottom[index]])); } } let fill; if (convex) { fill = pathOf(convexHull([...top, ...bottom]), true); } else { const parts = [pathOf(oriented(top), true), pathOf(oriented(bottom), true)]; for (let index = 0; index < count; index++) { if (!facing[index]) continue; const next = (index + 1) % count; parts.push(pathOf(oriented([top[index], top[next], bottom[next], bottom[index]]), true)); } fill = parts.join(""); } const buckets = Array.from({ length: SHADES }, () => []); if (height > 0) { const [lx, ly] = [-sinA, cosA]; for (let index = 0; index < count; index++) { if (!facing[index]) continue; const [nx, ny] = normals[index]; const lit = (nx * lx + ny * ly + 1) / 2; const shade = Math.min(SHADES - 1, Math.max(0, Math.floor(lit * SHADES))); const next = (index + 1) % count; buckets[shade].push(pathOf(oriented([top[index], top[next], bottom[next], bottom[index]]), true)); } } let bevel; if (options.bevel && options.bevel > 0) { const by = options.bevel; const inset = ring.map(([x, y], index) => { const [ax, ay] = normals[(index + count - 1) % count]; const [bx, by2] = normals[index]; const mx = ax + bx; const my = ay + by2; const length = Math.hypot(mx, my) || 1; return [x - (mx / length) * by, y - (my / length) * by]; }); const insetTop = inset.map(([x, y]) => iso([x, y, z + height], projection)); bevel = runsOf(facing, true) .map((run) => pathOf([top[run[0]], ...run.slice(1, -1).map((index) => insetTop[index]), top[run[run.length - 1]]])) .join(""); } return { fill, outline: outline.join(""), crease: crease.join(""), top: pathOf(oriented(top), true), shades: buckets.map((paths) => paths.join("")), bevel, }; } export const ringOf = (plan, steps = 8) => roundedPlan(plan, steps); export function slabOf(plan, z, height, projection, steps = 8, bevel = 0) { return extrude(ringOf(plan, steps), z, height, projection, { convex: true, bevel }); } export function circleRing(cx, cy, radius, steps = 40) { return Array.from({ length: steps }, (_, step) => { const angle = (step / steps) * Math.PI * 2; return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]; }); } export function cylinder(cx, cy, radius, z, height, projection, steps = 32, bevel = 0) { return extrude(circleRing(cx, cy, radius, steps), z, height, projection, { convex: true, bevel }); } export const insetPlan = ({ x, y, w, d, r }, by) => ({ x: x + by, y: y + by, w: w - by * 2, d: d - by * 2, r: Math.max(0, r - by) }); export function onTop(points, z, projection, closed = false) { return pathOf(points.map(([x, y]) => iso([x, y, z], projection)), closed); } export const planOutline = (plan, z, projection, steps = 8) => onTop(ringOf(plan, steps), z, projection, true); export const lineOnTop = (from, to, z, projection) => onTop([from, to], z, projection); export const segment = (from, to, projection) => pathOf([iso(from, projection), iso(to, projection)]); export const rise = (at, from, to, projection) => segment([at[0], at[1], from], [at[0], at[1], to], projection); export const ring = (cx, cy, radius, z, projection, steps = 40) => onTop(circleRing(cx, cy, radius, steps), z, projection, true); export const pointsAt = (points, projection) => points.map((point) => iso(point, projection)); export function dotGrid(x, y, columns, rows, gap, z, projection) { const dots = []; for (let row = 0; row < rows; row++) { for (let column = 0; column < columns; column++) dots.push(iso([x + column * gap, y + row * gap, z], projection)); } return dots; } export function corners({ x, y, w, d }, inset = 0) { return [ [x + inset, y + inset], [x + w - inset, y + inset], [x + inset, y + d - inset], [x + w - inset, y + d - inset], ]; } export function boxCorners({ x, y, w, d }, z0, z1) { const out = []; for (const px of [x, x + w]) for (const py of [y, y + d]) for (const z of [z0, z1]) out.push([px, py, z]); return out; } export function knurl(cx, cy, radius, z0, z1, count, projection, inset = 0.4) { const [vx, vy] = towardViewer(projection); const lines = []; for (let index = 0; index < count; index++) { const angle = (index / count) * Math.PI * 2; const nx = Math.cos(angle); const ny = Math.sin(angle); if (nx * vx + ny * vy < 0.15) continue; lines.push(segment([cx + radius * nx, cy + radius * ny, z0 + inset], [cx + radius * nx, cy + radius * ny, z1 - inset], projection)); } return lines.join(""); } export function sideArc(cx, cy, radius, z, projection, steps = 24) { const [vx, vy] = towardViewer(projection); const facing = Math.atan2(vy, vx); const points = []; for (let step = 0; step <= steps; step++) { const angle = facing - Math.PI / 2 + (step / steps) * Math.PI; points.push(iso([cx + radius * Math.cos(angle), cy + radius * Math.sin(angle), z], projection)); } return pathOf(points); } export function ringSeam(ring, z, projection) { const count = ring.length; const counter = signedArea(ring) > 0; const [vx, vy] = towardViewer(projection); const facing = ring.map(([ax, ay], index) => { const [bx, by] = ring[(index + 1) % count]; const dx = bx - ax; const dy = by - ay; const [nx, ny] = counter ? [dy, -dx] : [-dy, dx]; return nx * vx + ny * vy > 1e-9; }); return runsOf(facing, true).map((run) => pathOf(run.map((index) => iso([ring[index][0], ring[index][1], z], projection)))).join(""); } export const sideSeam = (plan, z, projection, steps = 8) => ringSeam(ringOf(plan, steps), z, projection); export function sideRing(center, radius, axis, projection, steps = 20) { const [px, py, pz] = center; const points = []; for (let step = 0; step < steps; step++) { const angle = (step / steps) * Math.PI * 2; const u = radius * Math.cos(angle); const v = radius * Math.sin(angle); points.push(iso(axis === "x" ? [px, py + u, pz + v] : [px + u, py, pz + v], projection)); } return pathOf(points, true); } export function coil(from, to, center, radius, turns, axis, projection, perTurn = 16) { const count = Math.max(1, Math.round(turns * perTurn)); const back = []; const front = []; let run = []; let near = null; for (let index = 0; index <= count; index++) { const share = index / count; const angle = share * turns * Math.PI * 2; const u = radius * Math.cos(angle); const v = radius * Math.sin(angle); const along = from + (to - from) * share; const point = axis === "x" ? [along, center[0] + u, center[1] + v] : [center[0] + u, along, center[1] + v]; const normal = axis === "x" ? [0, Math.cos(angle), Math.sin(angle)] : [Math.cos(angle), 0, Math.sin(angle)]; const facing = depthOf(normal, projection) > 0; const flat = iso(point, projection); if (near === null) near = facing; if (facing !== near) { run.push(flat); (near ? front : back).push(pathOf(run)); run = [flat]; near = facing; continue; } run.push(flat); } if (run.length > 1) (near ? front : back).push(pathOf(run)); return { back: back.join(""), front: front.join("") }; } export function topTicks(from, to, step, every, edge, z, lengths, projection, axis = "x") { const minor = []; const major = []; const first = Math.ceil(from / step) * step; for (let at = first, index = Math.round(first / step); at <= to + 1e-9; at += step, index++) { const big = index % every === 0; const length = big ? lengths[1] : lengths[0]; const a = axis === "x" ? [at, edge] : [edge, at]; const b = axis === "x" ? [at, edge + length] : [edge + length, at]; (big ? major : minor).push(lineOnTop(a, b, z, projection)); } return { minor: minor.join(""), major: major.join("") }; } export function sideTicks(from, to, step, every, face, top, lengths, projection, axis = "x") { const minor = []; const major = []; const first = Math.ceil(from / step) * step; for (let at = first, index = Math.round(first / step); at <= to + 1e-9; at += step, index++) { const big = index % every === 0; const length = big ? lengths[1] : lengths[0]; const a = axis === "x" ? [at, face, top] : [face, at, top]; const b = axis === "x" ? [at, face, top - length] : [face, at, top - length]; (big ? major : minor).push(segment(a, b, projection)); } return { minor: minor.join(""), major: major.join("") }; } export function radialTicks(cx, cy, radius, count, every, z, lengths, projection, from = 0, span = Math.PI * 2) { const minor = []; const major = []; const closed = Math.abs(span - Math.PI * 2) < 1e-9; const steps = closed ? count : count + 1; for (let index = 0; index < steps; index++) { const angle = from + (index / count) * span; const big = index % every === 0; const inner = radius - (big ? lengths[1] : lengths[0]); const sx = Math.sin(angle); const sy = -Math.cos(angle); (big ? major : minor).push(lineOnTop([cx + sx * inner, cy + sy * inner], [cx + sx * radius, cy + sy * radius], z, projection)); } return { minor: minor.join(""), major: major.join("") }; } export const haloOf = (plan, z, height, projection) => slabOf(plan, z, height, projection).fill; export function axisVector(projection, [dx, dy, dz]) { const origin = iso([0, 0, 0], projection); const moved = iso([dx, dy, dz], projection); return [moved[0] - origin[0], moved[1] - origin[1]]; } const fine = (value) => Math.round(value * 100) / 100; export function translateAlong(projection, by) { const [x, y] = axisVector(projection, by); return `translate(${fine(x)} ${fine(y)})`; } const thousandth = (value) => Math.round(value * 1000) / 1000; function axesAt(at, z, projection) { const origin = iso([at[0], at[1], z], projection); const along = (point) => { const [px, py] = iso(point, projection); return [px - origin[0], py - origin[1]]; }; return { origin, x: along([at[0] + 1, at[1], z]), y: along([at[0], at[1] + 1, z]), z: along([at[0], at[1], z + 1]) }; } const matrixOf = (u, v, origin) => `matrix(${thousandth(u[0])} ${thousandth(u[1])} ${thousandth(v[0])} ${thousandth(v[1])} ${thousandth(origin[0])} ${thousandth(origin[1])})`; export function holdStill(element, [dx, dy]) { const text = dx || dy ? `translate(${-dx} ${-dy})` : ""; element.querySelectorAll("[data-hold]").forEach((node) => (text ? node.setAttribute("transform", text) : node.removeAttribute("transform"))); } export function topMatrix(at, z, projection, along = "x") { const { origin, x, y } = axesAt(at, z, projection); return along === "x" ? matrixOf(x, y, origin) : matrixOf([-y[0], -y[1]], x, origin); } export function sideMatrix(at, z, projection, face) { const { origin, x, y, z: up } = axesAt(at, z, projection); const down = [-up[0], -up[1]]; return face === "left" ? matrixOf(x, down, origin) : matrixOf([-y[0], -y[1]], down, origin); } export function screenRise(height, projection) { const { cosE, k } = cameraOf(projection); return height * cosE * k; } export function fitProjection(points, width, height, { pad = 24, headroom = 0, azimuth = AZIMUTH, elevation = ELEVATION } = {}) { const unit = { origin: [0, 0], scale: 1, azimuth, elevation }; const flat = points.map((point) => iso(point, unit)); const xs = flat.map(([x]) => x); const ys = flat.map(([, y]) => y); const left = Math.min(...xs); const right = Math.max(...xs); const up = Math.min(...ys); const down = Math.max(...ys); const scale = Math.min((width - pad * 2) / (right - left), (height - pad * 2 - headroom) / (down - up)); return { origin: [width / 2 - ((left + right) / 2) * scale, headroom + (height - headroom) / 2 - ((up + down) / 2) * scale], scale, azimuth, elevation, }; } const escape = (text) => text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); const attrs = (pairs) => Object.entries(pairs) .filter(([, value]) => value !== undefined && value !== false) .map(([key, value]) => ` ${key}="${escape(String(value))}"`) .join(""); export function solidSvg(paths, { tone = "hi", crease = "faint", halo = false, lit = false, flat = false, className, inner = "" } = {}) { const parts = []; if (halo) parts.push(``); parts.push(``); if (!flat) { paths.shades.forEach((d, shade) => { if (d) parts.push(``); }); parts.push(``); } if (crease !== "none" && paths.crease) parts.push(``); if (crease !== "none" && paths.bevel) parts.push(``); if (inner) parts.push(inner); parts.push(``); return `${parts.join("")}`; } export function lineSvg(d, { tone = "lo", dotted = false, dashed = false, flow = false, className, free } = {}) { if (!d) return ""; return ``; } export function dotsSvg(points, { size = 0.5, tone = "mid", pulse = false } = {}) { const dots = points .map(([x, y, alpha], index) => ``) .join(""); return `${dots}`; } export function fadedSvg(list, style = {}) { const groups = new Map(); for (const entry of list) { const [d, alpha] = Array.isArray(entry) ? entry : [entry.d, entry.alpha]; if (!d || !(alpha > 0.02)) continue; const level = Math.min(1, Math.round(alpha * 20) / 20); groups.set(level, (groups.get(level) ?? "") + d); } return [...groups].map(([alpha, d]) => (alpha >= 1 ? lineSvg(d, style) : `${lineSvg(d, style)}`)).join(""); } export function fadeLineSvg(points, { fade = [0, 0], tone = "lo", className, stops = 6 } = {}) { if (points.length < 2) return ""; const ease = (t) => t * t * (3 - 2 * t); const hashOf = (text) => { let hash = 2166136261; for (let index = 0; index < text.length; index++) hash = Math.imul(hash ^ text.charCodeAt(index), 16777619); return (hash >>> 0).toString(36); }; const lengths = [0]; for (let index = 1; index < points.length; index++) lengths.push(lengths[index - 1] + Math.hypot(points[index][0] - points[index - 1][0], points[index][1] - points[index - 1][1])); const at = (length) => { let index = 1; while (index < points.length - 1 && lengths[index] < length) index++; const span = lengths[index] - lengths[index - 1] || 1; const t = Math.max(0, Math.min(1, (length - lengths[index - 1]) / span)); return [points[index - 1][0] + (points[index][0] - points[index - 1][0]) * t, points[index - 1][1] + (points[index][1] - points[index - 1][1]) * t]; }; const pieceOf = (from, to) => [at(from), ...points.filter((_, index) => lengths[index] > from + 1e-6 && lengths[index] < to - 1e-6), at(to)]; const total = lengths[lengths.length - 1]; if (total < 1e-6) return ""; let [start, end] = Array.isArray(fade) ? fade : [fade, fade]; if (start + end > total) { const share = total / (start + end); start *= share; end *= share; } const paint = `var(--anatomy-${tone})`; const ramp = (piece, rising) => { const d = pathOf(piece); const [x1, y1] = piece[0]; const [x2, y2] = piece[piece.length - 1]; const id = `iso-fade-${hashOf(`${d}|${tone}|${rising}`)}`; const marks = Array.from({ length: stops + 1 }, (_, index) => { const t = index / stops; const alpha = ease(rising ? t : 1 - t); return ``; }).join(""); const gradient = `${marks}`; return `${gradient}`; }; const parts = []; if (start > 0.05) parts.push(ramp(pieceOf(0, start), true)); if (total - end - start > 0.05) parts.push(lineSvg(pathOf(pieceOf(start, total - end)), { tone, className })); if (end > 0.05) parts.push(ramp(pieceOf(total - end, total), false)); return `${parts.join("")}`; } export function wireSvg(from, to, { via = [], tone = "lo", dotted = false, dashed = false, flow = false, ends = "both", size = 1.2 } = {}) { const d = pathOf([from, ...via, to]); const end = ([x, y]) => ``; return `${lineSvg(d, { tone, dotted, dashed, flow })}${ends === "both" || ends === "from" ? end(from) : ""}${ends === "both" || ends === "to" ? end(to) : ""}`; } export function faceTextSvg(transform, text, { size = 3, tone = "mid", anchor = "start" } = {}) { return `${escape(text)}`; } export function groupSvg(children, extra = {}) { return `${Array.isArray(children) ? children.join("") : children}`; } export function figureSvg({ width, height, label, body, standalone = false, className }) { const style = standalone ? `` : ""; return `${style}${Array.isArray(body) ? body.join("") : body}`; } function swatchSvg(mark) { if (mark === "raised" || mark === "flat" || mark === "lit") { const body = mark === "flat" ? "M2 6.5 8 3.5 14 6.5 8 9.5Z" : "M2 4.5 8 1.5 14 4.5 14 6 8 9 2 6Z"; const crease = mark === "flat" ? "" : ``; return ``; } return ``; } export function plateHtml({ fig, title, hint, readout = "", keys = [], caption, body }) { const corner = (name, text, extra = "") => `${escape(text)}`; const legend = keys.length || caption ? `
${keys.length ? `${keys.map((key) => `${swatchSvg(key.mark)}${escape(key.label)}`).join("")}` : ""}${caption ? `${escape(caption)}` : ""}
` : ""; return `
${body}
${legend}
`; } export function pageHtml({ title, theme = "dark", body, script = "", width = 650 }) { return `${escape(title)}
${body}
${script ? `` : ""}`; } const PAGE_CSS = ` *{box-sizing:border-box} html{color-scheme:dark} body[data-theme="light"]{color-scheme:light} body{margin:0;background:var(--anatomy-page);color:var(--anatomy-text);font:14px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased} .iso-page{width:min(__WIDTH__px,100% - 32px);margin:64px auto 96px} `; export const ISO_CSS = ` .iso{ --anatomy-page:#0b0b0b;--anatomy-card:#131313;--anatomy-text:#a3a3a3;--anatomy-strong:#e6e6e6;--anatomy-muted:#6e6e6e; --anatomy-paper:#131313;--anatomy-face:#1b1b1b;--anatomy-top:#1c1c1c; --anatomy-shade-0:#0f0f0f;--anatomy-shade-1:#121212;--anatomy-shade-2:#151515;--anatomy-shade-3:#181818; --anatomy-lit-top:#262626;--anatomy-lit-shade:#1d1d1d; --anatomy-hi:#d4d4d4;--anatomy-mid:#6a6a6a;--anatomy-lo:#3e3e3e;--anatomy-faint:#2a2a2a;--anatomy-lit:#ffffff; --anatomy-halo:#050505;--anatomy-halo-opacity:.9;--anatomy-dot:#e6e6e6; --anatomy-red:#ff6b6b;--anatomy-green:#6be0a0;--anatomy-blue:#6b9bff; --anatomy-weight:.6px;--anatomy-ease:cubic-bezier(.32,.72,0,1); --anatomy-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace; --anatomy-lift:inset 0 1px 0 0 #ffffff0a,inset 0 0 0 1px #ffffff06,0 0 0 1px #00000026,0 1px 1px -.5px #00000014,0 3px 3px -1.5px #00000014,0 6px 6px -3px #00000014,0 12px 12px -6px #00000014; } .iso[data-theme="light"]{ --anatomy-page:#f2f2ef;--anatomy-card:#fbfbfa;--anatomy-text:#57575a;--anatomy-strong:#18181a;--anatomy-muted:#8c8c90; --anatomy-paper:#fbfbfa;--anatomy-face:#ffffff;--anatomy-top:#ffffff; --anatomy-shade-0:#dcdcd9;--anatomy-shade-1:#e5e5e2;--anatomy-shade-2:#ededea;--anatomy-shade-3:#f4f4f1; --anatomy-lit-top:#ffffff;--anatomy-lit-shade:#eeeeeb; --anatomy-hi:#26262a;--anatomy-mid:#8a8a8f;--anatomy-lo:#b4b4b8;--anatomy-faint:#d4d4d6;--anatomy-lit:#000000; --anatomy-halo:#8a8a80;--anatomy-halo-opacity:.32;--anatomy-dot:#3a3a3e; --anatomy-red:#e5484d;--anatomy-green:#30a46c;--anatomy-blue:#3e63dd; --anatomy-lift:inset 0 1px 0 0 #ffffff,0 0 0 1px #0000000f,0 1px 1px -.5px #0000000a,0 3px 3px -1.5px #0000000a,0 6px 6px -3px #0000000a,0 12px 12px -6px #0000000a; } .iso-svg{display:block;width:100%;height:auto;overflow:visible} .iso-svg path,.iso-svg circle{vector-effect:non-scaling-stroke} .iso-fill{fill:var(--anatomy-paper);stroke:none} .iso-solid[data-lit]>.iso-fill{fill:var(--anatomy-face)} .iso-shade,.iso-top{stroke:none} .iso-shade[data-shade="0"]{fill:var(--anatomy-shade-0)} .iso-shade[data-shade="1"]{fill:var(--anatomy-shade-1)} .iso-shade[data-shade="2"]{fill:var(--anatomy-shade-2)} .iso-shade[data-shade="3"]{fill:var(--anatomy-shade-3)} .iso-top{fill:var(--anatomy-top)} .iso-solid[data-lit]>.iso-top{fill:var(--anatomy-lit-top)} .iso-solid[data-lit]>.iso-shade{fill:var(--anatomy-lit-shade)} .iso-solid[data-seamless]>.iso-shade,.iso-solid[data-seamless]>.iso-top{stroke-width:.4;stroke-linejoin:round} .iso-solid[data-seamless]>.iso-shade[data-shade="0"]{stroke:var(--anatomy-shade-0)} .iso-solid[data-seamless]>.iso-shade[data-shade="1"]{stroke:var(--anatomy-shade-1)} .iso-solid[data-seamless]>.iso-shade[data-shade="2"]{stroke:var(--anatomy-shade-2)} .iso-solid[data-seamless]>.iso-shade[data-shade="3"]{stroke:var(--anatomy-shade-3)} .iso-solid[data-seamless]>.iso-top{stroke:var(--anatomy-top)} .iso-solid[data-seamless][data-lit]>.iso-top{stroke:var(--anatomy-lit-top)} .iso-solid[data-seamless][data-lit]>.iso-shade{stroke:var(--anatomy-lit-shade)} .iso-solid[data-lit]>.iso-edge{stroke:var(--anatomy-lit)} .iso-halo{fill:var(--anatomy-halo);stroke:none;filter:blur(9px);opacity:var(--anatomy-halo-opacity);transform:translateY(3px)} .iso-line{fill:none;stroke:var(--anatomy-lo);stroke-width:var(--anatomy-weight);stroke-linecap:round;stroke-linejoin:round} .iso-line[data-tone="hi"]{stroke:var(--anatomy-hi)} .iso-line[data-tone="mid"]{stroke:var(--anatomy-mid)} .iso-line[data-tone="lo"]{stroke:var(--anatomy-lo)} .iso-line[data-tone="faint"]{stroke:var(--anatomy-faint)} .iso-line[data-tone="lit"]{stroke:var(--anatomy-lit)} .iso-line[data-tone="red"]{stroke:var(--anatomy-red)} .iso-line[data-tone="green"]{stroke:var(--anatomy-green)} .iso-line[data-tone="blue"]{stroke:var(--anatomy-blue)} .iso-line[data-dotted]{stroke-dasharray:.01 3.5;stroke-width:1} .iso-line[data-dashed]{stroke-dasharray:3 3} .iso-line[data-flow]{stroke-dasharray:2 6} .iso-wire-end{fill:var(--anatomy-paper);stroke:var(--anatomy-lo);stroke-width:var(--anatomy-weight)} .iso-wire[data-tone="hi"] .iso-wire-end{stroke:var(--anatomy-hi)} .iso-wire[data-tone="mid"] .iso-wire-end{stroke:var(--anatomy-mid)} .iso-wire[data-tone="faint"] .iso-wire-end{stroke:var(--anatomy-faint)} .iso-wire[data-tone="lit"] .iso-wire-end{stroke:var(--anatomy-lit)} .iso-dots circle{fill:var(--anatomy-dot);stroke:none;opacity:.32} .iso-dots[data-tone="hi"] circle{opacity:.85} .iso-dots[data-tone="mid"] circle{opacity:.45} .iso-dots[data-tone="lo"] circle{opacity:.22} .iso-dots[data-tone="faint"] circle{opacity:.12} .iso-face-text{fill:var(--anatomy-mid);font-family:var(--anatomy-mono);letter-spacing:.04em} .iso-face-text[data-tone="hi"]{fill:var(--anatomy-hi)} .iso-face-text[data-tone="lo"]{fill:var(--anatomy-lo)} .iso-face-text[data-tone="faint"]{fill:var(--anatomy-faint)} .tb-body{fill:var(--anatomy-shade-2);stroke:none} .tb-shine{fill:var(--anatomy-top);stroke:none} .tb-shade{fill:var(--anatomy-shade-0);stroke:none} .tb-ring{fill:none;stroke:var(--anatomy-lo);stroke-width:.45;stroke-linecap:butt;stroke-linejoin:round} .tb-ring[data-tone="mid"]{stroke:var(--anatomy-mid)} .tb-ring[data-tone="faint"]{stroke:var(--anatomy-faint)} .tb-ring[data-tone="hi"]{stroke:var(--anatomy-hi)} @keyframes iso-flow{to{stroke-dashoffset:-32}} @keyframes iso-pulse{0%,100%{opacity:.18}12%{opacity:1}40%{opacity:.18}} .iso-line[data-flow]{animation:iso-flow 1600ms linear infinite} .iso-dots circle[data-pulse]{animation:iso-pulse 1600ms steps(1,end) infinite;animation-delay:var(--pulse-at,0ms)} .iso-figure{margin:32px 0 36px} .iso-plate{position:relative;padding:52px 24px;border-radius:0;background:var(--anatomy-card);box-shadow:0 0 0 1px var(--anatomy-mid);--anatomy-paper:var(--anatomy-card)} .iso-plate-corners{position:absolute;inset:18px 20px;pointer-events:none} .iso-plate-corner{position:absolute;color:var(--anatomy-muted);font-family:var(--anatomy-mono);font-size:10.5px;line-height:1;letter-spacing:.04em;white-space:nowrap} .iso-plate-corner[data-corner="fig"]{top:0;left:0;color:var(--anatomy-text)} .iso-plate-corner[data-corner="title"]{top:0;right:0;text-transform:uppercase;letter-spacing:.1em} .iso-plate-corner[data-corner="hint"]{bottom:0;left:0;text-transform:uppercase;letter-spacing:.1em} .iso-plate-corner[data-corner="readout"]{right:0;bottom:0;color:var(--anatomy-text);font-variant-numeric:tabular-nums} .iso-plate .iso-svg{margin:0 auto} .iso-legend{display:grid;gap:8px;margin-top:12px;color:var(--anatomy-muted);font-size:13px;line-height:1.55} .iso-keys{display:flex;flex-wrap:wrap;gap:6px 18px} .iso-key{display:inline-flex;align-items:center;gap:7px} .iso-swatch{width:16px;height:10px;flex:none;overflow:visible} .iso-swatch path{fill:none;stroke:var(--anatomy-hi);stroke-width:1;vector-effect:non-scaling-stroke;stroke-linejoin:round;stroke-linecap:round} .iso-swatch[data-mark="raised"] path,.iso-swatch[data-mark="flat"] path{stroke:var(--anatomy-mid)} .iso-swatch[data-mark="lit"] path{stroke:var(--anatomy-lit)} .iso-swatch[data-mark="dim"] path{stroke:var(--anatomy-lo)} .iso-swatch[data-mark="dotted"] path{stroke-dasharray:.01 3;stroke-width:1.4} .iso-swatch[data-mark="dashed"] path{stroke-dasharray:3 2.5} .iso-swatch[data-mark="red"] path{stroke:var(--anatomy-red)} .iso-swatch[data-mark="green"] path{stroke:var(--anatomy-green)} .iso-swatch[data-mark="blue"] path{stroke:var(--anatomy-blue)} .iso-swatch-crease{stroke:var(--anatomy-lo)!important} @media (max-width:560px){.iso-plate{padding:48px 12px 70px}.iso-plate-corner[data-corner="hint"]{bottom:18px}} @media (prefers-reduced-motion:reduce){.iso-line[data-flow],.iso-dots circle[data-pulse]{animation:none}} `;