type UiPlanState = {
name: string;
description: string;
};
type UiPlanComponent = {
name: string;
description: string;
};
const DEFAULT_BOARD_SKETCHINESS = 38;
export type BuildUiPlanHtmlInput = {
title: string;
brief: string;
source?: string;
repoPath?: string | null;
states?: UiPlanState[];
components?: UiPlanComponent[];
implementationNotes?: string;
sketchiness?: number;
};
const DEFAULT_STATES: UiPlanState[] = [
{
name: "Review",
description:
"The plan opens directly on a full-width, high-fidelity mockup with the plan text pushed below the first visual review surface.",
},
{
name: "Comment",
description:
"Text selection and click-to-comment stay anchored to the closest visible UI element or text node.",
},
{
name: "Draw",
description:
"Drawing tools let the reviewer mark position, hierarchy, and layout problems on the mockup itself.",
},
{
name: "Agent handoff",
description:
"Once feedback exists, the primary action becomes sending structured comments to the inline agent or copying them for the host agent.",
},
{
name: "Mobile",
description:
"Responsive states show how commenting, drawing, and handoff work on narrow screens.",
},
];
const DEFAULT_COMPONENTS: UiPlanComponent[] = [
{
name: "Floating toolbar",
description:
"Compact controls for comment mode, send-to-agent, share, theme, app-shell toggle, and overflow actions.",
},
{
name: "Comment popover",
description:
"One-field compact comment composer with no category picker or coordinate metadata in the user-facing bubble.",
},
{
name: "Drawing controls",
description:
"Pointer, rectangle, arrow, and freehand tools that attach marks to the active mockup state.",
},
{
name: "Implementation map",
description:
"Vertical file tabs with concise intent, snippets, and editor-open controls below the UI mockups.",
},
];
export function buildUiPlanHtml(input: BuildUiPlanHtmlInput): string {
const title = escapeHtml(input.title || "UI Plan");
const brief = escapeHtml(
input.brief || "Review the UI direction before code.",
);
const source = escapeHtml(input.source || "agent");
const repoPath = input.repoPath ? escapeHtml(input.repoPath) : "";
const states = cleanStates(input.states);
const components = cleanComponents(input.components);
const hasTopCanvas = states.length > 0 || components.length > 0;
const implementationNotes = escapeHtml(
input.implementationNotes ||
"Keep code detail close to the design decisions: files, state ownership, actions, accessibility checks, and the smallest snippets needed to make the implementation shape obvious.",
);
const sketchiness = clampSketchiness(input.sketchiness);
return `
${title}
${hasTopCanvas ? renderTopVisualCanvas({ title, brief, source, repoPath, states, components, sketchiness }) : ""}
UI plan
${title}
${brief}
What Matters Most
The plan should be read like an interactive product spec: scan the flow first, then use the rich document blocks below to inspect states, edge cases, implementation seams, and feedback prompts.
${
states.length > 0
? `${states
.slice(0, 5)
.map(
(state) =>
`${escapeHtml(state.name)} ${escapeHtml(state.description)} `,
)
.join("")} `
: `No dedicated top wireframes were supplied, so this plan stays in document mode and keeps the review surface lightweight.
`
}
${states.length > 0 ? renderDocumentStateTabs(states) : ""}
${states.length > 1 ? renderDocumentFlowDiagram(states) : ""}
${components.length > 0 ? renderDocumentComponentTabs(components) : ""}
${renderDocumentImplementationFrame(implementationNotes)}
${renderDocumentReviewBlock(states, components)}
`;
}
function renderTopVisualCanvas(input: {
title: string;
brief: string;
source: string;
repoPath: string;
states: UiPlanState[];
components: UiPlanComponent[];
sketchiness: number;
}) {
const board = buildTopCanvasLayout(input.states, input.components);
return `
-
68%
+
${
input.states.length > 0
? `${renderBoardFlowConnectors(input.states)}
${input.states.map((state, index) => renderBoardStateFrame(state, index)).join("")}`
: ""
}
${
input.components.length > 0
? `
Interaction notes
${input.components.map((component, index) => renderBoardComponentFrame(component, index, board)).join("")}`
: ""
}
${renderCanvasAnnotationLayers(input.states, board, input.sketchiness)}
`;
}
function buildTopCanvasLayout(
states: UiPlanState[],
components: UiPlanComponent[],
) {
const secondaryCount = Math.max(0, states.length - 1);
const stateRows = Math.max(1, Math.ceil(secondaryCount / 4));
const componentRows =
components.length > 0 ? Math.ceil(components.length / 4) : 0;
const componentY = states.length > 0 ? 780 + (stateRows - 1) * 570 : 110;
return {
width: 2500,
height: componentY + Math.max(1, componentRows) * 260 + 170,
componentY,
implementationY: componentY + Math.max(1, componentRows) * 260 + 92,
};
}
function renderCanvasAnnotationLayers(
states: UiPlanState[],
board: ReturnType,
sketchiness: number,
) {
const primary = states[0];
const secondary = states[1];
const lastState =
states.length > 0 ? stateFrameLayout(states.length - 1) : null;
const handoffX = lastState
? Math.min(board.width - 380, lastState.x + lastState.width + 96)
: 1760;
return `
Read this like a design handoff.
Pan and zoom to compare frames.
Leave comments directly on artboards.
Scroll for the doc spec below.
Sketchiness ${sketchiness}%
${
primary
? `
${escapeHtml(primary.name)} frame
${escapeHtml(primary.description)}
`
: ""
}
${
secondary
? `
Comment path
${escapeHtml(secondary.description)}
`
: ""
}
Document continues below
Use the lower section for state tabs, diagrams, code tabs, review prompts, and implementation detail.
`;
}
function renderDocumentStateTabs(states: UiPlanState[]) {
return `
Screen States
Use these tabs to review each state without turning the plan into a long wall of repeated mockups.
${states
.map((state, index) => {
const id = docTabId("state", state.name, index);
return `${escapeHtml(state.name)} `;
})
.join("")}
${states
.map((state, index) => {
const id = docTabId("state", state.name, index);
return `
${renderInlineWireframe(state, index)}
State
${escapeHtml(state.name)}
${escapeHtml(state.description)}
Review checklist
Primary action and empty/error copy are visible.
Comment anchors can attach to the important UI region.
Mobile behavior is either shown or explicitly called out.
`;
})
.join("")}
`;
}
function renderInlineWireframe(state: UiPlanState, index: number) {
const isMobile = state.name.toLowerCase().includes("mobile");
return `
${escapeHtml(state.name)}
${[0, 1, 2, 3].map((item) => ` `).join("")}
`;
}
function renderDocumentFlowDiagram(states: UiPlanState[]) {
return `
Flow Diagram
A lightweight sketch diagram keeps the sequence visible after the top canvas has scrolled away.
${states
.slice(0, 5)
.map(
(state, index) => `
${index + 1} ${escapeHtml(state.name)}
${index < Math.min(states.length, 5) - 1 ? '
->
' : ""}
`,
)
.join("")}
`;
}
function renderDocumentComponentTabs(components: UiPlanComponent[]) {
return `
Interaction Details
Component notes stay close to a small sketch and focused constraints instead of becoming separate mini specs.
${components
.map((component, index) => {
const id = docTabId("component", component.name, index);
return `${escapeHtml(component.name)} `;
})
.join("")}
${components
.map((component, index) => {
const id = docTabId("component", component.name, index);
return `
Component
${escapeHtml(component.name)}
${escapeHtml(component.description)}
Action
`;
})
.join("")}
`;
}
function renderDocumentImplementationFrame(implementationNotes: string) {
return `
Implementation Map
${implementationNotes}
PlansPage.tsx Reader chrome, runtime, comments
create-ui-plan.ts Action contract and payload
ui-plan/SKILL.md Generation rules for agents
Document review surface
Keep the reader quiet: comment/drawing tools float outside the document, while rich tabs and diagrams stay inside the HTML plan.
const planShape = {
topCanvas: "when states or components exist" ,
document: "notion-like rich spec" ,
};
Create UI plan action
The action no longer needs a board boolean. Visual data creates the top canvas automatically; otherwise the generated plan remains document-only.
buildUiPlanHtml ({
title,
brief,
states,
components,
sketchiness,
});
/ui-plan skill
Agents should generate UI flow states when a visual review is useful, then use the document blocks for decisions, diagrams, tables, risks, and code handoff.
/ui-plan
- top canvas for key flows
- notion-style document below
- skip canvas when visuals add no value
`;
}
function renderDocumentReviewBlock(
states: UiPlanState[],
components: UiPlanComponent[],
) {
return `
Review Prompts
Area Ask Evidence
Flow Does the sequence make sense at a bird's-eye view? ${states.length || "No"} state${states.length === 1 ? "" : "s"}
Interaction Are the important controls close to the thing they affect? ${components.length || "No"} component note${components.length === 1 ? "" : "s"}
Handoff Can the agent read comments and implement without guessing? Anchors, code tabs, and checklist
`;
}
function docTabId(prefix: string, label: string, index: number) {
return `${prefix}-${tabId(label, index)}`;
}
function clampSketchiness(value: number | undefined) {
if (typeof value !== "number" || !Number.isFinite(value)) {
return DEFAULT_BOARD_SKETCHINESS;
}
return Math.max(0, Math.min(100, Math.round(value)));
}
function frameStyle(x: number, y: number, width: number, height: number) {
return `left:${x}px;top:${y}px;width:${width}px;height:${height}px;`;
}
function stateFrameLayout(index: number) {
if (index === 0) {
return { x: 80, y: 98, width: 780, height: 440, kind: "desktop" };
}
const secondary = index - 1;
return {
x: 1000 + (secondary % 4) * 430,
y: 98 + Math.floor(secondary / 4) * 570,
width: 302,
height: 500,
kind: "mobile",
};
}
function renderBoardStateFrame(state: UiPlanState, index: number) {
const layout = stateFrameLayout(index);
const label = escapeHtml(state.name);
const id = `board-state-${index}`;
const isDesktop = layout.kind === "desktop";
const inner = isDesktop
? renderBoardDesktopScreen(state, index)
: renderBoardPhoneScreen(state, index);
return `
${label}
${inner}
`;
}
function renderBoardFlowConnectors(states: UiPlanState[]) {
if (states.length < 2) return "";
return states
.slice(1)
.map((_, index) => {
const from = stateFrameLayout(index);
const to = stateFrameLayout(index + 1);
const startX = from.x + from.width;
const startY = from.y + from.height / 2;
const endX = to.x;
const endY = to.y + to.height / 2;
const left = Math.min(startX, endX) - 26;
const top = Math.min(startY, endY) - 42;
const width = Math.abs(endX - startX) + 52;
const height = Math.abs(endY - startY) + 84;
const localStartX = startX - left;
const localStartY = startY - top;
const localEndX = endX - left;
const localEndY = endY - top;
const c1x = localStartX + Math.max(80, width * 0.28);
const c2x = localEndX - Math.max(80, width * 0.28);
return ``;
})
.join("");
}
function renderBoardDesktopScreen(state: UiPlanState, index: number) {
return `
${escapeHtml(state.name)}
${escapeHtml(state.name)}
${escapeHtml(state.description)}
Primary
All
Active
Done
${[0, 1, 2, 3].map((item) => renderSketchTaskRow(item, index)).join("")}
`;
}
function renderBoardPhoneScreen(state: UiPlanState, index: number) {
const mode = state.name.toLowerCase();
const isForm =
mode.includes("add") || mode.includes("edit") || mode.includes("new");
const isDetail = mode.includes("detail") || mode.includes("task");
return `
9:41
${
isForm
? `
`
: isDetail
? `
${[0, 1, 2].map((item) => renderPhoneCheck(item)).join("")}
`
: `
All Active Done
${[0, 1, 2, 3].map((item) => renderPhoneTask(item, index)).join("")}
`
}
`;
}
function renderSketchTaskRow(item: number, stateIndex: number) {
const urgent = (item + stateIndex) % 3 === 0;
return `
${urgent ? "Soon" : "Later"}
`;
}
function renderPhoneTask(item: number, stateIndex: number) {
return `
${(item + stateIndex) % 2 === 0 ? "2 PM" : ""}
`;
}
function renderPhoneCheck(item: number) {
return `
`;
}
function renderBoardComponentFrame(
component: UiPlanComponent,
index: number,
board: { componentY: number },
) {
const x = 80 + (index % 4) * 410;
const y = board.componentY + Math.floor(index / 4) * 260;
return `
${escapeHtml(component.name)}
${escapeHtml(component.description)}
`;
}
function normalizeStates(states: UiPlanState[] | undefined) {
const cleaned = cleanStates(states);
return cleaned.length > 0 ? cleaned.slice(0, 8) : DEFAULT_STATES;
}
function normalizeComponents(components: UiPlanComponent[] | undefined) {
const cleaned = cleanComponents(components);
return cleaned.length > 0 ? cleaned.slice(0, 8) : DEFAULT_COMPONENTS;
}
function cleanStates(states: UiPlanState[] | undefined) {
return (states || [])
.map((state) => ({
name: state.name?.trim(),
description: state.description?.trim(),
}))
.filter(hasNameAndDescription)
.slice(0, 8);
}
function cleanComponents(components: UiPlanComponent[] | undefined) {
return (components || [])
.map((component) => ({
name: component.name?.trim(),
description: component.description?.trim(),
}))
.filter(hasNameAndDescription)
.slice(0, 8);
}
function hasNameAndDescription(
item: Partial,
): item is UiPlanState {
return Boolean(item.name && item.description);
}
function tabId(label: string, index: number) {
const slug = label
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
return `ui-${slug || "state"}-${index}`;
}
function escapeHtml(value: unknown) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
const UI_PLAN_JS = `
(() => {
let boardLayoutFrame = 0;
function requestBoardLayoutSync() {
if (boardLayoutFrame) return;
boardLayoutFrame = requestAnimationFrame(() => {
boardLayoutFrame = 0;
window.dispatchEvent(new Event("agent-native-plan-board-layout-change"));
});
}
function activateTab(tabset, target, focus) {
const buttons = Array.from(tabset.querySelectorAll("[data-tab-target]"));
const panels = Array.from(tabset.querySelectorAll("[data-tab-panel]"));
for (const button of buttons) {
const active = button.getAttribute("data-tab-target") === target;
button.classList.toggle("is-active", active);
button.setAttribute("aria-selected", active ? "true" : "false");
button.setAttribute("tabindex", active ? "0" : "-1");
if (active && focus) button.focus();
}
for (const panel of panels) {
panel.classList.toggle("is-active", panel.getAttribute("data-tab-panel") === target);
}
requestBoardLayoutSync();
}
for (const tabset of document.querySelectorAll("[data-plan-tabs]")) {
const buttons = Array.from(tabset.querySelectorAll("[data-tab-target]"));
if (buttons.length === 0) continue;
for (const button of buttons) {
button.addEventListener("click", () => activateTab(tabset, button.getAttribute("data-tab-target") || "", true));
}
activateTab(tabset, buttons.find((button) => button.classList.contains("is-active"))?.getAttribute("data-tab-target") || buttons[0].getAttribute("data-tab-target") || "", false);
}
const root = document.documentElement;
const viewport = document.querySelector("[data-board-viewport]");
const canvas = document.querySelector("[data-board-canvas]");
const zoomLabel = document.querySelector("[data-zoom-label]");
const roughMap = document.querySelector("[data-rough-map]");
if (!viewport || !canvas) return;
let zoom = 0.68;
let panX = 34;
let panY = 28;
let panStart = null;
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function applyCanvasTransform() {
canvas.style.transform = "translate(" + panX + "px," + panY + "px) scale(" + zoom + ")";
root.style.setProperty("--board-zoom", zoom.toFixed(3));
root.style.setProperty("--grid-size", (28 * zoom).toFixed(2) + "px");
root.style.setProperty("--grid-offset-x", panX.toFixed(2) + "px");
root.style.setProperty("--grid-offset-y", panY.toFixed(2) + "px");
if (zoomLabel) zoomLabel.textContent = Math.round(zoom * 100) + "%";
if (roughMap) roughMap.setAttribute("scale", String(Math.round((Number.parseFloat(root.style.getPropertyValue("--sketch")) || 0.38) * 100 / 12)));
requestBoardLayoutSync();
}
function setZoom(nextZoom, clientX, clientY) {
const rect = viewport.getBoundingClientRect();
const x = typeof clientX === "number" ? clientX - rect.left : rect.width / 2;
const y = typeof clientY === "number" ? clientY - rect.top : rect.height / 2;
const beforeX = (x - panX) / zoom;
const beforeY = (y - panY) / zoom;
zoom = clamp(nextZoom, 0.36, 1.35);
panX = x - beforeX * zoom;
panY = y - beforeY * zoom;
applyCanvasTransform();
}
function panCanvas(deltaX, deltaY) {
panX -= deltaX;
panY -= deltaY;
applyCanvasTransform();
}
document.querySelector("[data-zoom-out]")?.addEventListener("click", () => setZoom(zoom - 0.08));
document.querySelector("[data-zoom-in]")?.addEventListener("click", () => setZoom(zoom + 0.08));
viewport.addEventListener("wheel", (event) => {
if (root.classList.contains("an-plan-annotating")) return;
event.preventDefault();
if (event.metaKey || event.ctrlKey || event.altKey) {
const zoomStep = Math.min(0.04, Math.abs(event.deltaY) * 0.0045) * (event.deltaY > 0 ? -1 : 1);
setZoom(zoom + zoomStep, event.clientX, event.clientY);
return;
}
const horizontal = event.shiftKey && Math.abs(event.deltaX) < Math.abs(event.deltaY)
? event.deltaY
: event.deltaX;
const vertical = event.shiftKey ? event.deltaX : event.deltaY;
panCanvas(horizontal, vertical);
}, { passive: false });
viewport.addEventListener("pointerdown", (event) => {
if (root.classList.contains("an-plan-annotating")) return;
if (event.button !== 0) return;
const target = event.target instanceof Element ? event.target : null;
if (target?.closest(".canvas-controls,.board-frame,.board-card,.board-note,.canvas-helper-note,.canvas-annotation,.annotation-arrow,button,input,textarea,a,details,summary")) return;
panStart = { x: event.clientX, y: event.clientY, panX, panY };
viewport.classList.add("is-panning");
event.preventDefault();
});
document.addEventListener("pointermove", (event) => {
if (!panStart) return;
panX = panStart.panX + event.clientX - panStart.x;
panY = panStart.panY + event.clientY - panStart.y;
applyCanvasTransform();
});
for (const eventName of ["pointerup", "pointercancel"]) {
document.addEventListener(eventName, () => {
panStart = null;
viewport.classList.remove("is-panning");
});
}
applyCanvasTransform();
})();
`;
const UI_PLAN_CSS = `
@font-face { font-family: "Excalifont"; src: url("/fonts/Excalifont-Regular.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
:root { color-scheme: light; --bg: #faf9f7; --paper: #ffffff; --paper-soft: #f3f2ef; --ink: #18181b; --soft: #44403c; --muted: #78716c; --line: rgba(28,25,23,.13); --line-strong: rgba(28,25,23,.25); --canvas: #f3f2ef; --grid-line: rgba(28,25,23,.04); --accent: #2f6fed; --accent-soft: rgba(47,111,237,.1); --warning: #fff4bf; --note-ink: #4d4219; --wire-surface: #ffffff; --wire-soft: #f4f4f5; --wire-mark: #d4d4d8; --wire-rule: rgba(63,63,70,.18); --wire-frame-bg: #ffffff; --wire-line: rgba(39,39,42,.8); --wire-line-soft: rgba(39,39,42,.28); --wire-dot: #71717a; --board-shell: rgba(255,255,255,.94); --board-card-bg: #fff9dc; --board-card-bg-2: #e9f4ec; --board-card-bg-3: #edf1fb; --diagram-bg: #f3f2ef; --diagram-node-bg: #ffffff; --code-bg: #f5f5f6; --code-ink: #27272a; --syntax-keyword: #0b67d2; --syntax-string: #287d43; --tab-hover: rgba(28,25,23,.055); --wire-font: "Excalifont", "Comic Sans MS", "Bradley Hand", "Marker Felt", cursive; --doc-font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --mono-font: "SFMono-Regular", Consolas, "Liberation Mono", monospace; --shadow-soft: 0 18px 56px rgba(24,24,27,.1); --density-scale: 1; }
:root[data-agent-native-theme="dark"] { color-scheme: dark; --bg: #1f1e1d; --paper: #242423; --paper-soft: #2b2a2a; --ink: #f4f4f2; --soft: #d8d6d2; --muted: #aaa8a4; --line: rgba(244,244,242,.14); --line-strong: rgba(244,244,242,.26); --canvas: #1d1c1b; --grid-line: rgba(244,244,242,.024); --accent: #78a7ff; --accent-soft: rgba(120,167,255,.17); --warning: #3a3216; --note-ink: #ede7c7; --wire-surface: #202020; --wire-soft: #2a2a2a; --wire-mark: #686868; --wire-rule: rgba(244,244,242,.14); --wire-frame-bg: #202020; --wire-line: rgba(244,244,242,.78); --wire-line-soft: rgba(244,244,242,.26); --wire-dot: #aaa8a4; --board-shell: rgba(36,36,35,.95); --board-card-bg: #2a2929; --board-card-bg-2: #282828; --board-card-bg-3: #292a2e; --diagram-bg: #242423; --diagram-node-bg: #202020; --code-bg: #181817; --code-ink: #e7e5e1; --syntax-keyword: #7ab8ff; --syntax-string: #9ee6a3; --tab-hover: rgba(244,244,242,.08); --shadow-soft: 0 18px 56px rgba(0,0,0,.24); }
* { box-sizing: border-box; }
html { background: var(--bg); scroll-behavior: smooth; }
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--doc-font); line-height: 1.62; }
button, input, textarea { font: inherit; }
.rough-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.top-canvas-section { position: relative; height: 65vh; border-bottom: 1px solid var(--line); background: var(--canvas); overflow: hidden; }
.canvas-controls { position: absolute; z-index: 10; left: 12px; bottom: 12px; display: inline-flex; align-items: center; gap: 1px; border: 1px solid var(--line); border-radius: 7px; background: color-mix(in srgb, var(--paper) 84%, transparent); padding: 2px; box-shadow: 0 10px 28px rgba(0,0,0,.18); backdrop-filter: blur(14px); }
.canvas-controls button { min-width: 26px; height: 24px; border: 0; border-radius: 5px; background: transparent; color: var(--ink); padding: 0 7px; font-size: 12px; font-weight: 760; cursor: pointer; }
.canvas-controls button:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.canvas-viewport { position: absolute; inset: 0; overflow: hidden; cursor: grab; touch-action: none; background-image: linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); background-size: var(--grid-size) var(--grid-size); background-position: var(--grid-offset-x) var(--grid-offset-y); }
.canvas-viewport.is-panning { cursor: grabbing; user-select: none; }
.board-canvas { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }
.board-note, .board-frame, .board-card, .board-group-label, .flow-connector, .canvas-helper-note, .canvas-annotation, .annotation-arrow { position: absolute; }
.board-note, .board-frame, .board-card, .canvas-helper-note, .canvas-annotation { z-index: 2; color: var(--ink); cursor: default; }
.board-group-label { z-index: 2; display: flex; align-items: center; color: var(--ink); font: 760 18px/1 var(--doc-font); }
.board-group-label::before { display: none; }
.intro-note, .board-card, .canvas-helper-note { border: 1.7px solid var(--wire-line); border-radius: 8px; background: var(--board-shell); box-shadow: var(--shadow-soft); }
.intro-note { display: flex; flex-direction: column; justify-content: space-between; padding: 22px 24px; }
.intro-note::after, .board-card::after { content: ""; position: absolute; inset: calc(var(--sketch) * -3px); border: calc(1px + var(--sketch) * 1.35px) solid var(--wire-line-soft); border-radius: inherit; opacity: calc(var(--sketch) * .7); transform: translate(calc(var(--sketch) * 2px), calc(var(--sketch) * -1px)) rotate(calc(var(--sketch) * .26deg)); pointer-events: none; }
.eyebrow { margin: 0 0 10px; color: var(--muted); font: 750 11px/1.2 var(--doc-font); text-transform: uppercase; letter-spacing: 0; }
h1, h2, h3, p { margin-top: 0; }
.intro-note h2 { margin: 0 0 12px; font: 760 30px/1.08 var(--doc-font); letter-spacing: 0; }
.intro-note p, .board-card p, .frame-caption { color: var(--muted); font-size: 15px; }
.note-meta { display: flex; flex-wrap: wrap; gap: 7px; }
.note-meta span { max-width: 100%; overflow: hidden; border: 1px solid var(--line); border-radius: 999px; background: var(--paper-soft); padding: 5px 9px; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.flow-connector { z-index: 1; pointer-events: none; }
.flow-connector svg { position: absolute; inset: 0; overflow: visible; }
.flow-connector path { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linecap: round; stroke-dasharray: 8 8; opacity: .66; filter: url(#ui-plan-roughen); }
.flow-connector span { position: absolute; display: inline-flex; min-height: 26px; align-items: center; border: 1px solid var(--accent); border-radius: 999px; background: var(--paper); color: var(--accent); padding: 0 9px; font: 760 12px/1 var(--doc-font); }
.frame-label { position: absolute; left: 0; right: 0; top: -26px; display: flex; align-items: center; color: var(--muted); font: 650 15px/1.1 var(--doc-font); }
.frame-label span { display: none; }
.frame-label strong { color: var(--ink); font: 760 15px/1 var(--doc-font); }
.wire-window { position: absolute; inset: 0; overflow: hidden; border: 1.5px solid var(--wire-line); border-radius: 5px; background: var(--wire-surface); color: var(--ink); filter: url(#ui-plan-roughen); box-shadow: none; }
.window-bar { display: flex; height: 28px; align-items: center; gap: 6px; border-bottom: 1.4px solid var(--wire-line); padding: 0 9px; }
.window-bar span { width: 7px; height: 7px; border: 1.2px solid var(--wire-line); border-radius: 999px; }
.window-bar i { margin-left: 7px; color: var(--muted); font: 400 11px/1 var(--wire-font); font-style: normal; }
.desktop-shell { display: grid; height: calc(100% - 28px); grid-template-columns: 154px 1fr; }
.sketch-sidebar { display: flex; flex-direction: column; gap: 13px; border-right: 1.4px solid var(--wire-line); padding: 18px 15px; }
.sketch-sidebar b { margin-bottom: 4px; font: 400 15px/1 var(--wire-font); }
.sketch-sidebar i { display: block; height: calc(27px * var(--density-scale)); border: 1.3px solid var(--wire-line-soft); border-radius: 5px; background: var(--wire-soft); }
.sketch-sidebar i.is-active { background: var(--accent-soft); border-color: var(--accent); }
.sketch-main { min-width: 0; padding: 22px 24px; }
.screen-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; }
.screen-head h2 { margin-bottom: 4px; font: 400 25px/1.1 var(--wire-font); letter-spacing: 0; }
.screen-head p { max-width: 420px; margin: 0; color: var(--muted); font: 400 14px/1.35 var(--wire-font); }
.screen-head button, .handoff-actions button { min-height: 34px; border: 1.5px solid var(--accent); border-radius: 5px; background: var(--accent); color: #fff; padding: 0 14px; font: 750 13px/1 var(--doc-font); cursor: default; }
.pill-row { display: flex; flex-wrap: wrap; gap: 9px; margin: 20px 0 18px; }
.pill { display: inline-flex; min-height: 26px; align-items: center; border: 1.3px solid var(--wire-line); border-radius: 999px; background: var(--wire-surface); padding: 0 11px; font: 400 13px/1 var(--wire-font); }
.pill.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.task-list { display: grid; gap: calc(12px * var(--density-scale)); }
.task-row { display: grid; min-height: calc(52px * var(--density-scale)); grid-template-columns: 22px 1fr 58px; align-items: center; gap: 12px; border-top: 1.2px solid var(--wire-rule); }
.check { display: inline-block; width: 15px; height: 15px; border: 1.5px solid var(--wire-line); border-radius: 4px; background: var(--wire-surface); }
.check.checked { background: var(--accent); box-shadow: inset 0 0 0 3px #fff; }
.task-row b, .task-row i, .phone-task b, .phone-task i, .phone-check i, .notes-lines i, .task-title, .input-line, .textarea-line { display: block; border-radius: 999px; background: var(--wire-mark); }
.task-row b { width: 54%; height: 10px; margin-bottom: 8px; }
.task-row i { width: 34%; height: 8px; }
.task-row em { justify-self: end; border: 1.2px solid var(--wire-line); border-radius: 999px; padding: 3px 7px; color: var(--muted); font: 400 11px/1 var(--wire-font); font-style: normal; }
.task-row em.hot { border-color: #cf5432; color: #cf5432; }
.frame-caption, .annotation-note { display: none; }
.phone-frame { padding: 0; background: transparent; }
.phone-shell { position: absolute; inset: 0; overflow: hidden; border: 1.5px solid var(--wire-line); border-radius: 25px; background: var(--wire-surface); color: var(--ink); filter: url(#ui-plan-roughen); box-shadow: none; }
.phone-status { display: flex; height: 24px; align-items: center; gap: 4px; padding: 0 13px; color: var(--muted); font: 650 10px/1 var(--doc-font); }
.phone-status span { flex: 1; }
.phone-status i { width: 12px; height: 4px; border-radius: 99px; background: var(--wire-dot); }
.phone-header { display: grid; height: 40px; grid-template-columns: 54px 1fr 54px; align-items: center; border-bottom: 1.3px solid var(--wire-line); padding: 0 9px; text-align: center; }
.phone-header strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 400 14px/1 var(--wire-font); }
.phone-header button { border: 0; background: transparent; color: var(--accent); padding: 0; font: 750 11px/1 var(--doc-font); }
.phone-list, .phone-form, .phone-detail { padding: 17px 14px; }
.phone-list .pill-row { margin-top: 0; gap: 7px; }
.phone-task { display: grid; min-height: calc(48px * var(--density-scale)); grid-template-columns: 18px 1fr 38px; align-items: center; gap: 8px; border-bottom: 1px solid var(--wire-rule); }
.phone-task b { width: 68%; height: 8px; margin-bottom: 7px; }
.phone-task i { width: 43%; height: 7px; }
.phone-task em { color: var(--muted); font: 400 10px/1 var(--wire-font); font-style: normal; }
.phone-form label { display: block; margin: 13px 0 5px; color: var(--muted); font: 750 9px/1 var(--doc-font); text-transform: uppercase; letter-spacing: 0; }
.input-line { height: 32px; border: 1.2px solid var(--wire-line); background: transparent; }
.textarea-line { height: 72px; border: 1.2px solid var(--wire-line); border-radius: 5px; background: transparent; }
.chip-grid { display: flex; flex-wrap: wrap; gap: 7px; }
.chip-grid span { border: 1.2px solid var(--wire-line); border-radius: 999px; padding: 5px 8px; font: 400 11px/1 var(--wire-font); }
.chip-grid span.is-active { border-color: var(--accent); color: var(--accent); }
.task-title { width: 84%; height: 21px; margin-bottom: 18px; }
.priority-row { display: flex; gap: 8px; margin-bottom: 26px; }
.priority-row span { width: 66px; height: 22px; border: 1.2px solid var(--accent); border-radius: 999px; background: var(--accent-soft); }
.notes-lines { display: grid; gap: 9px; margin-bottom: 24px; }
.notes-lines i { height: 9px; }
.notes-lines i:nth-child(2) { width: 82%; }
.notes-lines i:nth-child(3) { width: 48%; }
.check-list { display: grid; gap: 15px; }
.phone-check { display: grid; grid-template-columns: 18px 1fr; gap: 8px; align-items: center; }
.phone-check i { height: 8px; }
.board-card { background: transparent; padding: 0; }
.component-card:nth-of-type(2n), .component-card:nth-of-type(3n) { background: transparent; }
.component-card h3 { margin: 0 0 10px; font: 760 19px/1.2 var(--doc-font); letter-spacing: 0; }
.component-card p { font: 500 14px/1.45 var(--doc-font); }
.component-mini { display: none; }
.canvas-helper-note { padding: 18px 20px; background: color-mix(in srgb, var(--paper) 86%, transparent); font-family: var(--doc-font); }
.canvas-helper-note strong { display: block; margin-bottom: 10px; font-size: 16px; font-weight: 760; }
.canvas-helper-note ul { display: grid; gap: 5px; margin: 0 0 12px; padding-left: 19px; }
.canvas-helper-note p { margin: 0; color: var(--muted); }
.canvas-helper-note span { color: var(--accent); font-size: 13px; }
.canvas-helper-note.muted { background: var(--paper-soft); color: var(--soft); }
.canvas-annotation { font-family: var(--doc-font); pointer-events: auto; }
.canvas-annotation h4 { margin: 0 0 7px; color: var(--ink); font-size: 15px; line-height: 1.15; font-weight: 760; letter-spacing: 0; }
.canvas-annotation p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
.canvas-annotation ul { display: grid; gap: 5px; margin: 0; padding-left: 18px; color: var(--soft); font-size: 13px; line-height: 1.35; }
.canvas-annotation.handoff-note h4, .canvas-annotation.component-annotation h4 { color: var(--ink); }
.canvas-annotation.handoff-note p { margin-top: 12px; color: var(--accent); font-size: 12px; }
.canvas-annotation.quiet-note { color: var(--muted); }
.annotation-arrow { z-index: 1; pointer-events: none; }
.annotation-arrow svg { display: block; width: 100%; height: 100%; overflow: visible; }
.annotation-arrow path { fill: none; stroke: var(--accent); stroke-width: 2.1; stroke-linecap: round; stroke-linejoin: round; opacity: .72; filter: url(#ui-plan-roughen); }
.notion-plan { width: min(910px, calc(100vw - 44px)); margin: 0 auto; padding: 88px 0 118px; }
.doc-cover { padding-bottom: 34px; border-bottom: 1px solid var(--line); }
.doc-kicker { margin: 0 0 10px; color: var(--muted); font-size: 12px; font-weight: 760; letter-spacing: 0; text-transform: uppercase; }
.doc-cover h1 { margin: 0 0 18px; font-size: clamp(38px, 5vw, 62px); line-height: 1.02; letter-spacing: -.03em; }
.doc-lede { max-width: 780px; margin: 0; color: var(--soft); font-size: clamp(19px, 2.4vw, 25px); line-height: 1.48; letter-spacing: -.012em; }
.doc-block { padding: 34px 0; border-bottom: 1px solid var(--line); scroll-margin-top: 20px; }
.doc-block h2 { margin: 0 0 12px; font-size: clamp(25px, 3vw, 34px); line-height: 1.14; letter-spacing: -.024em; }
.doc-block h3 { margin: 0 0 10px; font-size: 22px; line-height: 1.2; letter-spacing: -.014em; }
.doc-block > p, .state-notes p, .component-copy p, .file-detail p, .doc-note { color: var(--soft); }
.doc-note { border-left: 3px solid var(--line-strong); margin: 18px 0 0; padding-left: 14px; }
.doc-list { display: grid; gap: 12px; margin: 22px 0 0; padding: 0; list-style: none; counter-reset: doc-list; }
.doc-list li { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; counter-increment: doc-list; }
.doc-list li::before { content: counter(doc-list); display: grid; width: 25px; height: 25px; place-items: center; border-radius: 6px; background: var(--paper-soft); color: var(--muted); font-size: 12px; font-weight: 760; }
.doc-list strong { display: block; margin-bottom: 2px; }
.doc-list strong, .doc-list span { grid-column: 2; }
.doc-list span { display: block; color: var(--soft); }
.visual-tabs { display: grid; gap: 18px; margin-top: 20px; }
.tab-list { display: inline-flex; width: fit-content; max-width: 100%; gap: 8px; border: 0; overflow-x: auto; }
.tab-button { min-height: 38px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--muted); padding: 0 12px; font-weight: 650; white-space: nowrap; cursor: pointer; }
.tab-button:hover { color: var(--ink); background: var(--tab-hover); }
.tab-button.is-active { border-color: var(--ink); color: var(--ink); }
.tab-button.is-active:hover { color: var(--ink); background: transparent; }
.tab-panel { display: none; }
.tab-panel.is-active { display: block; }
.state-spec { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(280px, .92fr); gap: 28px; align-items: start; }
.inline-wireframe { overflow: hidden; border: 1px solid var(--wire-line-soft); border-radius: 8px; background: var(--wire-surface); filter: url(#ui-plan-roughen); box-shadow: none; }
.inline-wireframe.is-mobile { max-width: 380px; border-radius: 28px; }
.wireframe-top { display: flex; height: 34px; align-items: center; gap: 6px; border-bottom: 1px solid var(--wire-line-soft); padding: 0 10px; }
.wireframe-top span { width: 8px; height: 8px; border: 1px solid var(--wire-line-soft); border-radius: 999px; }
.wireframe-top strong { margin-left: 8px; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font: 400 13px/1 var(--wire-font); }
.wireframe-body { display: grid; min-height: 330px; grid-template-columns: 92px 1fr; }
.inline-wireframe.is-mobile .wireframe-body { grid-template-columns: 1fr; }
.wireframe-body aside { display: grid; align-content: start; gap: 10px; border-right: 1px solid var(--wire-line-soft); padding: 16px; }
.inline-wireframe.is-mobile aside { display: none; }
.wireframe-body aside i { height: 22px; border: 1px solid var(--wire-line-soft); border-radius: 5px; background: var(--wire-soft); }
.wireframe-body aside i.active { border-color: var(--accent); background: var(--accent-soft); }
.wireframe-body main { padding: 24px; }
.wireframe-body main b, .wireframe-body main p, .wireframe-grid span { display: block; border-radius: 999px; background: var(--wire-mark); }
.wireframe-body main b { width: 58%; height: 28px; margin-bottom: 20px; }
.wireframe-body main p { width: 84%; height: 10px; margin-bottom: 11px; }
.wireframe-body main p.short { width: 46%; }
.wireframe-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 26px; }
.wireframe-grid span { height: 86px; border-radius: 7px; border: 1px solid var(--wire-line-soft); background: var(--wire-soft); }
.wireframe-grid span.accent { border-color: var(--accent); background: var(--accent-soft); }
.state-notes { display: grid; gap: 14px; }
details { border-top: 1px solid var(--line); padding-top: 12px; }
summary { color: var(--ink); font-weight: 720; cursor: pointer; }
details ul { margin: 12px 0 0; padding-left: 18px; color: var(--soft); }
.sketch-flow-diagram { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; border: 1px solid var(--line); border-radius: 8px; background: var(--diagram-bg); padding: 22px; font-family: var(--doc-font); filter: url(#ui-plan-roughen); }
.diagram-step { display: flex; align-items: center; gap: 14px; }
.diagram-node { min-width: 148px; border: 1.5px solid var(--wire-line); border-radius: 8px; background: var(--diagram-node-bg); padding: 12px 14px; }
.diagram-node span { display: inline-grid; width: 22px; height: 22px; place-items: center; margin-right: 8px; border-radius: 999px; background: var(--accent); color: #fff; font-family: var(--doc-font); font-size: 12px; font-weight: 800; }
.diagram-node strong { font-weight: 760; }
.diagram-arrow { color: var(--accent); font-size: 28px; }
.component-spec { display: grid; grid-template-columns: minmax(0, .9fr) minmax(260px, 1.1fr); gap: 28px; align-items: center; }
.component-mini-spec { display: grid; grid-template-columns: 1fr 1fr auto; gap: 11px; align-items: center; border: 1px solid var(--wire-line-soft); border-radius: 8px; background: var(--wire-surface); padding: 20px; min-height: 180px; filter: url(#ui-plan-roughen); }
.component-mini-spec span, .component-mini-spec i { min-height: 30px; border: 1px solid var(--wire-line-soft); border-radius: 6px; background: var(--wire-soft); }
.component-mini-spec button { min-height: 34px; border: 1px solid var(--accent); border-radius: 6px; background: var(--accent); color: #fff; padding: 0 14px; font-weight: 760; }
.component-mini-spec i { grid-column: span 3; min-height: 52px; }
.file-map-preview { display: grid; grid-template-columns: minmax(220px, .36fr) minmax(0, 1fr); margin-top: 20px; }
.file-list { border-right: 1px solid var(--line); }
.file-tab { display: grid; width: 100%; gap: 4px; border: 0; border-bottom: 1px solid var(--line); background: transparent; color: var(--muted); padding: 16px 14px; text-align: left; cursor: pointer; }
.file-tab:hover { background: var(--tab-hover); color: var(--ink); }
.file-tab.is-active { color: var(--ink); box-shadow: inset 3px 0 0 var(--accent); }
.file-tab strong { font: 760 13px/1.25 var(--mono-font); }
.file-tab span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.file-panels { min-width: 0; }
.file-detail { min-width: 0; padding: 22px 24px; }
pre { margin: 18px 0 0; overflow: auto; border: 1px solid var(--line); border-radius: 8px; background: var(--code-bg); padding: 18px 20px; color: var(--code-ink); font: 13px/1.65 var(--mono-font); }
pre code, pre code * { background: transparent !important; }
.syntax-keyword { color: var(--syntax-keyword); }
.syntax-string { color: var(--syntax-string); }
.doc-table { width: 100%; margin-top: 16px; border-collapse: collapse; font-size: 14px; }
.doc-table th, .doc-table td { border-bottom: 1px solid var(--line); padding: 12px 10px; text-align: left; vertical-align: top; }
.doc-table th { color: var(--muted); font-size: 12px; font-weight: 760; text-transform: uppercase; letter-spacing: 0; }
.doc-table td { color: var(--soft); }
.doc-table td:first-child { color: var(--ink); font-weight: 720; }
@media (max-width: 900px) {
.top-canvas-section { height: 65vh; }
.notion-plan { width: min(100vw - 28px, 910px); padding-top: 58px; }
.state-spec, .component-spec, .file-map-preview { grid-template-columns: 1fr; }
.file-list { border-right: 0; }
.doc-cover h1 { font-size: clamp(38px, 10vw, 56px); }
}
@media (max-width: 620px) {
.canvas-controls { left: 14px; bottom: 14px; }
.wireframe-body { grid-template-columns: 1fr; }
.wireframe-body aside { display: none; }
.sketch-flow-diagram { align-items: stretch; flex-direction: column; }
.diagram-step { align-items: stretch; flex-direction: column; }
.diagram-arrow { transform: rotate(90deg); width: fit-content; margin-left: 24px; }
}
`;