From e1abc774028fea4fa60ea6de8ac3fd461c8f26a2 Mon Sep 17 00:00:00 2001 From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:04:08 +1000 Subject: [PATCH 1/3] feat: add mascots, 3D objects, and motion patterns The demo had one desk pair and the cube. This gives the system a dot, a page, and a desk, plus sheets and a token that turn, and the loading and agent-loop treatments a session already needs. Co-authored-by: Cursor --- assets/mark-accent.svg | 8 +- assets/mark-seal.svg | 25 +- assets/mark-warm.svg | 8 +- assets/mascot-desk.svg | 38 +++ assets/mascot-dot.svg | 13 + assets/mascot-page.svg | 27 ++ assets/mascot.svg | 42 +-- scripts/build-identity.py | 222 ++++++++++--- src/renderer/components/AgentLoop.tsx | 26 ++ src/renderer/components/DeskObjects.tsx | 57 ++++ src/renderer/components/FrameLoader.tsx | 32 ++ src/renderer/components/Loader.tsx | 24 ++ src/renderer/components/Skeleton.tsx | 18 + src/renderer/demo.css | 67 +++- src/renderer/demo.tsx | 128 ++++++++ src/renderer/design-system/components.css | 380 ++++++++++++++++++++++ 16 files changed, 1020 insertions(+), 95 deletions(-) create mode 100644 assets/mascot-desk.svg create mode 100644 assets/mascot-dot.svg create mode 100644 assets/mascot-page.svg create mode 100644 src/renderer/components/AgentLoop.tsx create mode 100644 src/renderer/components/DeskObjects.tsx create mode 100644 src/renderer/components/FrameLoader.tsx create mode 100644 src/renderer/components/Loader.tsx create mode 100644 src/renderer/components/Skeleton.tsx diff --git a/assets/mark-accent.svg b/assets/mark-accent.svg index 72b8b68..d55d04f 100644 --- a/assets/mark-accent.svg +++ b/assets/mark-accent.svg @@ -4,13 +4,13 @@ - - + + - - + + diff --git a/assets/mark-seal.svg b/assets/mark-seal.svg index 22b519a..103ef81 100644 --- a/assets/mark-seal.svg +++ b/assets/mark-seal.svg @@ -31,19 +31,6 @@ - - - - - - - - - - - - - @@ -73,12 +60,12 @@ - - - - - - + + + + + + diff --git a/assets/mark-warm.svg b/assets/mark-warm.svg index d01e589..19f45a5 100644 --- a/assets/mark-warm.svg +++ b/assets/mark-warm.svg @@ -4,13 +4,13 @@ - - + + - - + + diff --git a/assets/mascot-desk.svg b/assets/mascot-desk.svg new file mode 100644 index 0000000..28f8b1b --- /dev/null +++ b/assets/mascot-desk.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/mascot-dot.svg b/assets/mascot-dot.svg new file mode 100644 index 0000000..8330f39 --- /dev/null +++ b/assets/mascot-dot.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/mascot-page.svg b/assets/mascot-page.svg new file mode 100644 index 0000000..215e2f5 --- /dev/null +++ b/assets/mascot-page.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/mascot.svg b/assets/mascot.svg index 210bd9f..dedc9e1 100644 --- a/assets/mascot.svg +++ b/assets/mascot.svg @@ -1,27 +1,31 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - + - + - - - - + + + + diff --git a/scripts/build-identity.py b/scripts/build-identity.py index d42dfb3..ade466e 100644 --- a/scripts/build-identity.py +++ b/scripts/build-identity.py @@ -28,9 +28,7 @@ INK = logo.INK EDGE = logo.EDGE ACCENT = "#6a9fcc" -DEEP = "#0d1116" WARM = "#e1b06e" -WARM_INK = "#13110f" def svg(inner: str, view: str) -> str: @@ -136,6 +134,9 @@ def outline_glyphs(font_path: Path, text: str, size: float) -> list[tuple[str, f def seal_with_upem(upem: int) -> str: ticks = [] for i in range(72): + # Leave the bottom of the ring clear for the word. + if 30 <= i <= 42: + continue angle = math.radians(i * 5 - 90) inner = 64 if i % 6 else 58 outer = 72 @@ -144,24 +145,18 @@ def seal_with_upem(upem: int) -> str: ticks.append( f'' ) - size = 13.0 + size = 11.0 letters = outline_glyphs(DEPARTURE, "PIDEX", size) - gap = size * 0.42 - width = sum(advance for _, advance in letters) + gap * (len(letters) - 1) - radius = 52 - cursor = -width / 2 - glyphs = [] + gap = size * 0.2 + tracked = sum(advance for _, advance in letters) + gap * (len(letters) - 1) scale = size / upem + cursor = 90 - tracked / 2 + glyphs = [] for commands, advance in letters: - mid = cursor + advance / 2 - theta = mid / radius - x = 90 + radius * math.sin(theta) - y = 90 - radius * math.cos(theta) - deg = math.degrees(theta) if commands: glyphs.append( f'' + f'transform="translate({cursor:.2f} 154) scale({scale:.6f} {-scale:.6f})"/>' ) cursor += advance + gap cube = logo.cube_markup(logo.PRIMARY, "pi-face-seal") @@ -172,7 +167,7 @@ def seal_with_upem(upem: int) -> str: f'', *ticks, *glyphs, - '', + '', cube, "", ] @@ -282,44 +277,171 @@ def xy(p: float, q: float) -> tuple[float, float]: return [xy(1 + (p - 1) * scale, q * scale) for p, q in corners] -def mascot() -> str: - body, _ = iso_cube(78, 78, 78, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) - dx = 78 * 0.8660254037844386 - dy = 78 * 0.5 - # Back tip of the top face is (cx, cy - dy). The Pi's top-left lands there. +def pi_on_face(cx: float, cy: float, e: float, clip_id: str = "mascot-face") -> str: + dx = e * 0.8660254037844386 + dy = e * 0.5 b = dy / logo.PI_SPAN - ty = (78 - dy) - 2 * b * 165.29 - pi_scale_matrix = ( - f"matrix({dx / logo.PI_SPAN:.8f} {b:.8f} {-dx / logo.PI_SPAN:.8f} {b:.8f} 78.00 {ty:.2f})" + # Back tip of the top face is (cx, cy - dy). The Pi's top-left lands there. + ty = (cy - dy) - 2 * b * 165.29 + matrix = f"matrix({dx / logo.PI_SPAN:.8f} {b:.8f} {-dx / logo.PI_SPAN:.8f} {b:.8f} {cx:.2f} {ty:.2f})" + face = " ".join(f"{x:.2f},{y:.2f}" for x, y in diamond(cx, cy, e, 1)) + return "\n".join( + [ + "", + f' ', + "", + f'', + f' ', + f' ', + f' ', + " ", + "", + ] ) - dot_cube, _ = iso_cube(168, 118, 36, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) - dot = diamond(168, 118, 36, 0.5) - dot_pts = " ".join(f"{x:.2f},{y:.2f}" for x, y in dot) - left_foot, _ = iso_cube(52, 168, 20, "#243246", "#31475c", "#1a2432", EDGE) - right_foot, _ = iso_cube(104, 168, 20, "#243246", "#31475c", "#1a2432", EDGE) + + +def iso_prism( + cx: float, + cy: float, + width: float, + depth: float, + height: float, + top: str, + right: str, + left: str, + edge: str, +) -> str: + """Top-face centre at (cx, cy). A cube is width = depth = height.""" + rx = 0.8660254037844386 + + def pt(across: float, along: float, down: float = 0) -> tuple[float, float]: + return (cx + across * (width / 2) * rx, cy + along * (depth / 2) + down) + + back, right_pt, front, left_pt = pt(0, -1), pt(1, 0), pt(0, 1), pt(-1, 0) + + def drop(p: tuple[float, float]) -> tuple[float, float]: + return (p[0], p[1] + height) + + def poly(points: list[tuple[float, float]], fill: str) -> str: + body = " ".join(f"{x:.2f},{y:.2f}" for x, y in points) + return ( + f'' + ) + + return "\n".join( + [ + poly([left_pt, front, drop(front), drop(left_pt)], left), + poly([right_pt, front, drop(front), drop(right_pt)], right), + poly([back, right_pt, front, left_pt], top), + ] + ) + + +FOOT = "#243246" +FOOT_RIGHT = "#31475c" +FOOT_LEFT = "#1a2432" + + +def feet_at(cx: float, ground: float, spread: float, size: float) -> list[str]: + cy = ground - 1.5 * size + size * 0.45 + return [ + iso_cube(cx - spread, cy, size, FOOT, FOOT_RIGHT, FOOT_LEFT, EDGE)[0], + iso_cube(cx + spread, cy, size, FOOT, FOOT_RIGHT, FOOT_LEFT, EDGE)[0], + ] + + +def standing_dot(cx: float, ground: float, e: float) -> list[str]: + cy = ground - 1.5 * e + body, _ = iso_cube(cx, cy, e, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) + mark = " ".join(f"{x:.2f},{y:.2f}" for x, y in diamond(cx, cy, e, 0.48)) + return [ + *feet_at(cx, ground, e * 0.28, e * 0.28), + body, + f'', + ] + + +def mascot() -> str: + # Both cubes share a front-bottom line, with a gap between them. + ground = 196.0 + body_e = 86.0 + body_cx, body_cy = 82.0, ground - 1.5 * body_e + side_e = 40.0 + side_cx, side_cy = 196.0, ground - 1.5 * side_e + body, _ = iso_cube(body_cx, body_cy, body_e, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) + side, _ = iso_cube(side_cx, side_cy, side_e, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) + dot = " ".join(f"{x:.2f},{y:.2f}" for x, y in diamond(side_cx, side_cy, side_e, 0.48)) + feet = [ + *feet_at(body_cx, ground, 26, 18), + *feet_at(side_cx, ground, 10, 11), + ] return svg( "\n".join( [ - '', - left_foot, - right_foot, + '', + *feet, body, - "", - ' ', - f' ', - " ", - "", - '', - f' ', - f' ', - f' ', - " ", - "", - dot_cube, - f'', + pi_on_face(body_cx, body_cy, body_e), + side, + f'', ] ), - "0 0 230 230", + "0 0 260 230", + ) + + +def mascot_dot() -> str: + ground = 158.0 + return svg( + "\n".join( + [ + '', + *standing_dot(80, ground, 70), + ] + ), + "0 0 160 186", + ) + + +def mascot_page() -> str: + """A standing sheet. Thin in depth, so the broad face reads as a page.""" + ground = 198.0 + page = iso_prism(86, 64, 112, 14, 128, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) + return svg( + "\n".join( + [ + '', + *feet_at(86, ground, 22, 14), + page, + '', + pi_group(58, 124, 46, INK), + *standing_dot(186, ground, 42), + ] + ), + "0 0 250 228", + ) + + +def mascot_desk() -> str: + """The pair at a low slab, with a sheet on the desk.""" + ground = 168.0 + desk = iso_prism(124, 158, 200, 86, 16, "#243246", "#31475c", "#1a2432", EDGE) + page = iso_prism(132, 118, 78, 12, 70, "#2a3b50", "#3d536b", "#1c2838", EDGE) + return svg( + "\n".join( + [ + '', + *feet_at(64, ground, 16, 12), + iso_cube(64, ground - 1.5 * 58, 58, logo.TOP, logo.RIGHT, logo.LEFT, EDGE)[0], + pi_on_face(64, ground - 1.5 * 58, 58, "desk-face"), + desk, + page, + '', + *standing_dot(200, 162, 30), + ] + ), + "0 0 270 224", ) @@ -330,15 +452,21 @@ def main() -> None: write("mark-stencil.svg", stencil()) write("mark-blueprint.svg", blueprint()) write("mark-seal.svg", seal_with_upem(upem)) - write("mark-accent.svg", logo.build_mark_svg(logo.Palette("#6a9fcc", "#4b607c", "#2c4058", "#d5e4f2", DEEP), "pi-face-accent")) + write( + "mark-accent.svg", + logo.build_mark_svg(logo.Palette("#6a9fcc", "#4d6d8f", "#24384c", "#d5e4f2", INK), "pi-face-accent"), + ) write( "mark-warm.svg", - logo.build_mark_svg(logo.Palette(WARM, "#a56a42", "#6e4632", "#f4e4cf", WARM_INK), "pi-face-warm"), + logo.build_mark_svg(logo.Palette(WARM, "#8f6248", "#5c4034", "#f4e4cf", INK), "pi-face-warm"), ) write("logo-wordmark.svg", wordmark()) write("logo-editorial.svg", editorial()) write("logo-label.svg", label_lockup(upem)) write("mascot.svg", mascot()) + write("mascot-dot.svg", mascot_dot()) + write("mascot-page.svg", mascot_page()) + write("mascot-desk.svg", mascot_desk()) if __name__ == "__main__": diff --git a/src/renderer/components/AgentLoop.tsx b/src/renderer/components/AgentLoop.tsx new file mode 100644 index 0000000..5012b1f --- /dev/null +++ b/src/renderer/components/AgentLoop.tsx @@ -0,0 +1,26 @@ +const STEPS = [ + ['think', 'ds-loop-node--think'], + ['reply', 'ds-loop-node--reply'], + ['tool', 'ds-loop-node--tool'], + ['result', 'ds-loop-node--result'], +] as const; + +function AgentLoop() { + return ( +
+
+ + {STEPS.map(([label, place]) => ( + + {label} + + ))} +
+
+ ); +} + +export { AgentLoop }; diff --git a/src/renderer/components/DeskObjects.tsx b/src/renderer/components/DeskObjects.tsx new file mode 100644 index 0000000..4879d45 --- /dev/null +++ b/src/renderer/components/DeskObjects.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from 'react'; +import { PiMark } from './PiMark'; + +function ObjectStage({ label, children }: { label: string; children: ReactNode }) { + return ( +
+
{children}
+ + {label} + +
+ ); +} + +function SheetTurn() { + return ( + +
+
+ + + +
+
+
+ + ); +} + +function PanelStack() { + return ( + +
+
+
+
+
+ + ); +} + +function TokenSpin() { + return ( + +
+
+ +
+
+ +
+
+
+ ); +} + +export { PanelStack, SheetTurn, TokenSpin }; diff --git a/src/renderer/components/FrameLoader.tsx b/src/renderer/components/FrameLoader.tsx new file mode 100644 index 0000000..c067487 --- /dev/null +++ b/src/renderer/components/FrameLoader.tsx @@ -0,0 +1,32 @@ +const CORNERS = ['tl', 'tr', 'bl', 'br'] as const; + +function Plane({ back = false }: { back?: boolean }) { + const plane = ['ds-frame-loader-plane', back ? 'ds-frame-loader-plane--back' : 'ds-frame-loader-plane--front'] + .filter(Boolean) + .join(' '); + return ( +
+ {CORNERS.map(corner => ( + + ))} +
+ ); +} + +function FrameLoader({ label = 'loading' }: { label?: string }) { + return ( +
+
+
+ + +
+
+ + {label} + +
+ ); +} + +export { FrameLoader }; diff --git a/src/renderer/components/Loader.tsx b/src/renderer/components/Loader.tsx new file mode 100644 index 0000000..42c3a9c --- /dev/null +++ b/src/renderer/components/Loader.tsx @@ -0,0 +1,24 @@ +type LoaderProps = { + label?: string; + variant?: 'ticks' | 'rule'; +}; + +function Loader({ label = 'loading', variant = 'ticks' }: LoaderProps) { + return ( + + {variant === 'ticks' ? ( +
+
+

Styles

+
+ + + + + + +
+
+ + + +
+
+ +
+

Mascot

+
+ +
+ +
+
+
+

Desk pair

+

The mark, stood up.

+

+ The cube carries the Pi. The smaller cube is the square dot, set on the same ground. + Same faces, same rim. +

+
+
+
+ + + +
+
+ +
+

Objects

+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+

Colour

@@ -123,6 +203,54 @@ function Demo() {
+
+

Patterns

+
+ +
+ + +
+ + +
+ + + +

+ › + +

+
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+ + + +
+

Session

+

Think, reply, tool, result.

+

+ The four steps a session already runs. The mark travels the square, and the step + lights as it passes. +

+
+
+
+

Components

diff --git a/src/renderer/design-system/components.css b/src/renderer/design-system/components.css index 7ad11b1..5f85806 100644 --- a/src/renderer/design-system/components.css +++ b/src/renderer/design-system/components.css @@ -341,3 +341,383 @@ border-top: 1px solid var(--line-soft); margin: 0; } + +/* Patterns -------------------------------------------------------------- */ + +/* pi.dev page grid: 4px fine lines, 20px heavy lines. */ +.ds-blueprint { + background-color: var(--bg-canvas); + background-image: + repeating-linear-gradient(90deg, hsl(218 60% 80% / 0.16) 0 1px, transparent 1px 20px), + repeating-linear-gradient(0deg, hsl(218 60% 80% / 0.16) 0 1px, transparent 1px 20px), + repeating-linear-gradient(90deg, hsl(218 60% 80% / 0.05) 0 1px, transparent 1px 4px), + repeating-linear-gradient(0deg, hsl(218 60% 80% / 0.05) 0 1px, transparent 1px 4px); +} + +.ds-hatch { + background-color: var(--bg-deep); + background-image: repeating-linear-gradient( + -45deg, + transparent 0 7px, + var(--line-soft) 7px 8px + ); +} + +/* Loading --------------------------------------------------------------- */ + +.ds-loader { + display: inline-flex; + width: fit-content; + align-items: center; + justify-content: flex-start; + gap: 0.7rem; + color: var(--muted-strong); + font-family: var(--accent-mono); + font-size: 0.66rem; + letter-spacing: 0.14em; + line-height: 1; + text-transform: uppercase; +} + +.ds-loader-ticks { + display: inline-flex; + gap: 0.28rem; +} + +.ds-loader-ticks span { + width: 0.42rem; + height: 0.42rem; + background: var(--line); + animation: ds-tick 1.2s steps(1, end) infinite; +} + +.ds-loader-ticks span:nth-child(2) { animation-delay: 0.15s; } +.ds-loader-ticks span:nth-child(3) { animation-delay: 0.3s; } +.ds-loader-ticks span:nth-child(4) { animation-delay: 0.45s; } + +@keyframes ds-tick { + 0%, + 22% { background: var(--accent); } + 23%, + 100% { background: var(--line); } +} + +.ds-scan { + position: relative; + display: block; + width: 8.5rem; + height: 2px; + overflow: hidden; + background: var(--line); +} + +.ds-scan::after { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 42%; + background: linear-gradient(90deg, transparent, var(--accent), transparent); + animation: ds-scan 1.2s steps(7, end) infinite; +} + +@keyframes ds-scan { + from { transform: translateX(-120%); } + to { transform: translateX(280%); } +} + +.ds-skeleton { + display: grid; + gap: 0.9rem; +} + +.ds-skeleton-row { + display: grid; + gap: 0.38rem; +} + +.ds-skeleton-row span { + display: block; + height: 0.55rem; + background: var(--panel-soft); + animation: ds-blink 1.6s steps(1, end) infinite; +} + +.ds-skeleton-row span:first-child { width: 78%; } +.ds-skeleton-row span:last-child { width: 34%; } + +.ds-skeleton-row:nth-child(2) span { animation-delay: 0.15s; } +.ds-skeleton-row:nth-child(3) span { animation-delay: 0.3s; } +.ds-skeleton-row:nth-child(4) span { animation-delay: 0.45s; } + +.ds-caret { + display: inline-block; + width: 0.5ch; + height: 0.95em; + background: var(--accent); + vertical-align: text-bottom; + animation: ds-blink 1.05s steps(1, end) infinite; +} + +/* 3D frame loader. Corner brackets, not a cube. */ + +.ds-frame-loader { + display: grid; + justify-items: center; + gap: 0.9rem; +} + +.ds-frame-loader-scene { + width: 96px; + height: 96px; + perspective: 340px; +} + +.ds-frame-loader-spin { + position: relative; + width: 100%; + height: 100%; + transform-style: preserve-3d; + animation: ds-frame-turn 7.5s linear infinite; +} + +@keyframes ds-frame-turn { + from { transform: rotateX(-24deg) rotateY(0deg); } + to { transform: rotateX(-24deg) rotateY(360deg); } +} + +.ds-frame-loader-plane { + position: absolute; + inset: 10px; + border: 1px solid var(--line-strong); + transform-style: preserve-3d; +} + +.ds-frame-loader-plane--front { transform: translateZ(28px); } +.ds-frame-loader-plane--back { + transform: translateZ(-28px); + border-color: var(--accent-border-strong); +} + +.ds-frame-loader-corner { + position: absolute; + width: 16px; + height: 16px; + color: var(--moonstone); +} + +.ds-frame-loader-plane--back .ds-frame-loader-corner { color: var(--accent); } + +.ds-frame-loader-corner--tl { top: -1px; left: -1px; border-top: 2px solid; border-left: 2px solid; } +.ds-frame-loader-corner--tr { top: -1px; right: -1px; border-top: 2px solid; border-right: 2px solid; } +.ds-frame-loader-corner--bl { bottom: -1px; left: -1px; border-bottom: 2px solid; border-left: 2px solid; } +.ds-frame-loader-corner--br { bottom: -1px; right: -1px; border-bottom: 2px solid; border-right: 2px solid; } + +/* Agent loop ------------------------------------------------------------ */ + +.ds-loop { + padding: 1.15rem 1.5rem 1.35rem; +} + +.ds-loop-stage { + position: relative; + width: min(100%, 240px); + margin: 0 auto; +} + +.ds-loop-svg { + display: block; + width: 100%; + height: auto; + color: var(--line-strong); +} + +.ds-loop-track { + fill: none; + stroke: currentColor; + stroke-width: 1; +} + +.ds-loop-head { + fill: none; + stroke: var(--accent); + stroke-width: 2; + stroke-dasharray: 48 720; + animation: ds-loop-run 4.8s linear infinite; +} + +@keyframes ds-loop-run { + to { stroke-dashoffset: -768; } +} + +.ds-loop-node { + position: absolute; + padding: 0.28rem 0.46rem 0.34rem; + border: 1px solid var(--line-soft); + background: var(--panel); + color: var(--muted); + font-family: var(--accent-mono); + font-size: 0.62rem; + letter-spacing: 0.14em; + line-height: 1; + text-transform: uppercase; + animation: ds-loop-on 4.8s steps(1, end) infinite; +} + +.ds-loop-node:nth-child(3) { animation-delay: 1.2s; } +.ds-loop-node:nth-child(4) { animation-delay: 2.4s; } +.ds-loop-node:nth-child(5) { animation-delay: 3.6s; } + +.ds-loop-node--think { top: 0; left: 0; transform: translate(-18%, -42%); } +.ds-loop-node--reply { top: 0; right: 0; transform: translate(18%, -42%); } +.ds-loop-node--tool { bottom: 0; right: 0; transform: translate(18%, 42%); } +.ds-loop-node--result { bottom: 0; left: 0; transform: translate(-18%, 42%); } + +@keyframes ds-loop-on { + 0%, + 24% { + color: var(--text); + border-color: var(--accent); + background: var(--accent-surface-soft); + } + 25%, + 100% { + color: var(--muted); + border-color: var(--line-soft); + background: var(--panel); + } +} + +/* 3D objects. Sheets and a token, not cubes. */ + +.ds-object { + display: grid; + justify-items: center; + gap: 0.85rem; +} + +.ds-object-scene { + width: 140px; + height: 140px; + display: grid; + place-items: center; + perspective: 460px; +} + +.ds-sheet-spin, +.ds-stack-spin, +.ds-token-spin { + position: relative; + transform-style: preserve-3d; + animation: ds-object-turn 9s linear infinite; +} + +@keyframes ds-object-turn { + from { transform: rotateX(-20deg) rotateY(0deg); } + to { transform: rotateX(-20deg) rotateY(360deg); } +} + +.ds-sheet-spin { width: 74px; height: 100px; } + +.ds-sheet { + position: absolute; + inset: 0; + display: grid; + align-content: center; + justify-items: center; + gap: 7px; + background: #2a3b50; + border: 1px solid #6e849c; + color: var(--moonstone); + backface-visibility: hidden; + transform: translateZ(4px); +} + +.ds-sheet i { + display: block; + width: 34px; + height: 2px; + background: #6e849c; +} + +.ds-sheet--back { + background: #1c2838; + transform: rotateY(180deg) translateZ(4px); +} + +.ds-stack-spin { width: 108px; height: 86px; } + +.ds-panel3 { + position: absolute; + inset: 0; + background: #2a3b50; + border: 1px solid #6e849c; + transform: translateZ(var(--z)) rotateY(-6deg); +} + +.ds-panel3::before { + content: ""; + position: absolute; + left: 16px; + right: 28px; + top: 22px; + height: 2px; + background: #6e849c; +} + +.ds-panel3:nth-child(1) { --z: 20px; border-color: var(--accent); } +.ds-panel3:nth-child(2) { --z: 0px; background: #24384c; } +.ds-panel3:nth-child(3) { --z: -20px; background: #1c2838; } + +.ds-token-spin { + width: 78px; + height: 78px; + animation-name: ds-token-turn; +} + +@keyframes ds-token-turn { + from { transform: rotateX(-12deg) rotateY(0deg); } + to { transform: rotateX(-12deg) rotateY(360deg); } +} + +.ds-token { + position: absolute; + inset: 0; + display: grid; + place-items: center; + background: #2a3b50; + border: 1px solid var(--moonstone); + color: var(--moonstone); + backface-visibility: hidden; + transform: translateZ(5px); +} + +.ds-token--back { + background: var(--moonstone); + color: #0d1116; + transform: rotateY(180deg) translateZ(5px); +} + +@media (prefers-reduced-motion: reduce) { + .ds-loader-ticks span, + .ds-scan::after, + .ds-skeleton-row span, + .ds-caret, + .ds-frame-loader-spin, + .ds-loop-head, + .ds-loop-node, + .ds-sheet-spin, + .ds-stack-spin, + .ds-token-spin { + animation: none; + } + + .ds-frame-loader-spin, + .ds-sheet-spin, + .ds-stack-spin { transform: rotateX(-24deg) rotateY(-32deg); } + .ds-token-spin { transform: rotateX(-12deg) rotateY(-28deg); } + .ds-loop-node--think { + color: var(--text); + border-color: var(--accent); + background: var(--accent-surface-soft); + } +} From 8aaf9203c262d328212df6a0186cf3030ca78843 Mon Sep 17 00:00:00 2001 From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:16:56 +1000 Subject: [PATCH 2/3] feat: add round marks and more mascot SVGs Disc, arc, and orbit marks plus bead, signal, halo, turn, twins, and cairn mascots, generated by build-identity.py. Co-Authored-By: Claude Opus 5.5 --- assets/mark-arc.svg | 5 + assets/mark-disc.svg | 8 ++ assets/mark-orbit.svg | 6 + assets/mascot-bead.svg | 12 ++ assets/mascot-cairn.svg | 19 ++++ assets/mascot-halo.svg | 39 +++++++ assets/mascot-signal.svg | 31 ++++++ assets/mascot-turn.svg | 14 +++ assets/mascot-twins.svg | 24 ++++ scripts/build-identity.py | 226 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 384 insertions(+) create mode 100644 assets/mark-arc.svg create mode 100644 assets/mark-disc.svg create mode 100644 assets/mark-orbit.svg create mode 100644 assets/mascot-bead.svg create mode 100644 assets/mascot-cairn.svg create mode 100644 assets/mascot-halo.svg create mode 100644 assets/mascot-signal.svg create mode 100644 assets/mascot-turn.svg create mode 100644 assets/mascot-twins.svg diff --git a/assets/mark-arc.svg b/assets/mark-arc.svg new file mode 100644 index 0000000..d3570ea --- /dev/null +++ b/assets/mark-arc.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/mark-disc.svg b/assets/mark-disc.svg new file mode 100644 index 0000000..da79b98 --- /dev/null +++ b/assets/mark-disc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/mark-orbit.svg b/assets/mark-orbit.svg new file mode 100644 index 0000000..fa679a5 --- /dev/null +++ b/assets/mark-orbit.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/mascot-bead.svg b/assets/mascot-bead.svg new file mode 100644 index 0000000..f7112cc --- /dev/null +++ b/assets/mascot-bead.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/mascot-cairn.svg b/assets/mascot-cairn.svg new file mode 100644 index 0000000..c9e649f --- /dev/null +++ b/assets/mascot-cairn.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/mascot-halo.svg b/assets/mascot-halo.svg new file mode 100644 index 0000000..7e71bb1 --- /dev/null +++ b/assets/mascot-halo.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/mascot-signal.svg b/assets/mascot-signal.svg new file mode 100644 index 0000000..8b45699 --- /dev/null +++ b/assets/mascot-signal.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/mascot-turn.svg b/assets/mascot-turn.svg new file mode 100644 index 0000000..2fde385 --- /dev/null +++ b/assets/mascot-turn.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/mascot-twins.svg b/assets/mascot-twins.svg new file mode 100644 index 0000000..d35b09d --- /dev/null +++ b/assets/mascot-twins.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/build-identity.py b/scripts/build-identity.py index ade466e..5e45135 100644 --- a/scripts/build-identity.py +++ b/scripts/build-identity.py @@ -445,6 +445,223 @@ def mascot_desk() -> str: ) +def sphere(cx: float, cy: float, r: float, clip_id: str, *, glint: bool = True) -> str: + """A round bead: dark base, lit face, and a moonstone glint.""" + parts = [ + "", + f' ', + "", + f'', + f' ', + f' ', + ] + if glint: + parts.append( + f' ' + ) + parts += [ + "", + f'', + ] + return "\n".join(parts) + + +def ground_shadow(cx: float, y: float, rx: float) -> str: + return f'' + + +def mascot_signal() -> str: + """A cube with the Pi, and a bead held up on a stem.""" + ground = 168.0 + e = 64.0 + cx = 90.0 + cy = ground - 1.5 * e + tip_y = cy - e * 0.5 + head_r = 16.0 + head_y = tip_y - head_r - 14 + body, _ = iso_cube(cx, cy, e, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) + return svg( + "\n".join( + [ + ground_shadow(cx, ground + 14, 62), + *feet_at(cx, ground, 16, 12), + body, + pi_on_face(cx, cy, e, "signal-face"), + ( + f'' + ), + sphere(cx, head_y, head_r, "signal-head"), + ] + ), + "0 0 180 196", + ) + + +def mascot_twins() -> str: + """The pair, drawn as two beads on one ground.""" + ground = 148.0 + big_r, small_r = 46.0, 24.0 + big_x, small_x = 74.0, 156.0 + big_y, small_y = ground - big_r, ground - small_r + size = 46.0 + return svg( + "\n".join( + [ + ground_shadow(112, ground + 14, 92), + sphere(big_x, big_y, big_r, "twin-big", glint=False), + pi_group(big_x - size / 2, big_y - size / 2 + 2, size, INK), + sphere(small_x, small_y, small_r, "twin-small"), + ] + ), + "0 0 210 176", + ) + + +def mascot_cairn() -> str: + """Three cubes stacked. Each one sits on the centre of the face below.""" + ground = 186.0 + cx = 96.0 + sizes = (68.0, 44.0, 26.0) + cursor = ground + placed: list[tuple[float, float, float]] = [] + for e in sizes: + cy = cursor - 1.5 * e + placed.append((cx, cy, e)) + cursor = cy + e * 0.35 + parts = [ground_shadow(cx, ground + 12, 72), *feet_at(cx, ground, 18, 13)] + for x, cy, e in placed: + parts.append(iso_cube(x, cy, e, logo.TOP, logo.RIGHT, logo.LEFT, EDGE)[0]) + top_x, top_cy, top_e = placed[-1] + dot = " ".join(f"{x:.2f},{y:.2f}" for x, y in diamond(top_x, top_cy, top_e, 0.46)) + parts.append(f'') + return svg("\n".join(parts), "0 0 190 214") + + +def mascot_turn() -> str: + """The open arc, with the centre dot grown into a bead.""" + cx, cy, radius = 112.0, 96.0, 68.0 + bead_r = 30.0 + return svg( + "\n".join( + [ + ground_shadow(cx, cy + bead_r + 16, 52), + ( + f'' + ), + sphere(cx, cy, bead_r, "turn-bead"), + ( + f'' + ), + ] + ), + "0 0 200 168", + ) + + +def mascot_halo() -> str: + """The standing cube, with a ring in the plane of its top face.""" + ground = 164.0 + e = 58.0 + cx = 104.0 + cy = ground - 1.5 * e + rx, ry = 84.0, 40.0 + body, _ = iso_cube(cx, cy, e, logo.TOP, logo.RIGHT, logo.LEFT, EDGE) + hidden = body.replace(logo.TOP, "#000").replace(logo.RIGHT, "#000").replace(logo.LEFT, "#000").replace(EDGE, "#000") + return svg( + "\n".join( + [ + ground_shadow(cx, ground + 14, 86), + "", + ' ', + ' ', + hidden, + " ", + "", + *feet_at(cx, ground, 14, 11), + body, + pi_on_face(cx, cy, e, "halo-face"), + ( + f'' + ), + sphere(cx + rx, cy, 9, "halo-bead"), + ] + ), + "0 0 220 196", + ) + + +def disc() -> str: + # The Pi's square must sit inside the circle, so size is the inscribed square. + size = 84 + origin = 80 - size / 2 + return svg( + "\n".join( + [ + f'', + '', + pi_group(origin, origin, size, INK), + ] + ), + "0 0 160 160", + ) + + +def arc() -> str: + # A turn: centre, radius, and an open curve with round caps. + return svg( + "\n".join( + [ + f'', + f'', + ( + f'' + ), + ] + ), + "0 0 160 160", + ) + + +def orbit() -> str: + return svg( + "\n".join( + [ + f'', + f'', + f'', + f'', + ] + ), + "0 0 160 160", + ) + + +def bead() -> str: + """The square dot, drawn as a sphere.""" + return svg( + "\n".join( + [ + '', + "", + ' ', + "", + '', + ' ', + ' ', + f' ', + "", + f'', + ] + ), + "0 0 160 168", + ) + + def main() -> None: font = TTFont(DEPARTURE) upem = font["head"].unitsPerEm @@ -467,6 +684,15 @@ def main() -> None: write("mascot-dot.svg", mascot_dot()) write("mascot-page.svg", mascot_page()) write("mascot-desk.svg", mascot_desk()) + write("mark-disc.svg", disc()) + write("mark-arc.svg", arc()) + write("mark-orbit.svg", orbit()) + write("mascot-bead.svg", bead()) + write("mascot-signal.svg", mascot_signal()) + write("mascot-twins.svg", mascot_twins()) + write("mascot-cairn.svg", mascot_cairn()) + write("mascot-turn.svg", mascot_turn()) + write("mascot-halo.svg", mascot_halo()) if __name__ == "__main__": From 5676f5d32e73c1fa8c1b054ecdd3e2c662a32f61 Mon Sep 17 00:00:00 2001 From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:16:57 +1000 Subject: [PATCH 3/3] feat: add live mascots, Glyph family, stamps, and schematic - Blink, Prompt, and Fold mascots with idle, running, needs input, blocked, and ready states - Blink tones, gear, session roster avatars, and a block-character version with pi.dev skins - pi.dev set: Pip, Brackets, Caret, Glyph, and Stamp shapes in ink, blueprint, paper, and warm looks - Glyph family: Peek, Carry, Walk, Assemble, Extrude, wordmark, terminal, and sizes - Stamps: label, seal, ticket, postmark, review, stickers, worn ink, and press-in motion - Blueprint schematic sheet of the Glyph with dimensions, exploded view, parts list, and state schema - Round loaders and objects (ring, hoop, orb) Co-Authored-By: Claude Opus 5.5 --- src/renderer/components/GlyphFamily.tsx | 327 ++++ src/renderer/components/LiveMascots.tsx | 436 +++++ src/renderer/components/PiSet.tsx | 241 +++ src/renderer/components/Round.tsx | 47 + src/renderer/components/Schematic.tsx | 339 ++++ src/renderer/components/Stamps.tsx | 252 +++ src/renderer/demo.css | 529 +++++- src/renderer/demo.tsx | 696 +++++++- src/renderer/design-system/components.css | 1897 ++++++++++++++++++++- 9 files changed, 4760 insertions(+), 4 deletions(-) create mode 100644 src/renderer/components/GlyphFamily.tsx create mode 100644 src/renderer/components/LiveMascots.tsx create mode 100644 src/renderer/components/PiSet.tsx create mode 100644 src/renderer/components/Round.tsx create mode 100644 src/renderer/components/Schematic.tsx create mode 100644 src/renderer/components/Stamps.tsx diff --git a/src/renderer/components/GlyphFamily.tsx b/src/renderer/components/GlyphFamily.tsx new file mode 100644 index 0000000..52f502d --- /dev/null +++ b/src/renderer/components/GlyphFamily.tsx @@ -0,0 +1,327 @@ +import { useEffect, useId, useState, type CSSProperties } from 'react'; +import { usePrefersReducedMotion, type MascotState } from './LiveMascots'; + +/** + * The Glyph family: the Pi mark stood up as a character. The P carries the face, the square + * dot is its companion. Everything here reads the look and state from a surrounding PiSurface. + */ + +// The mark on its 4×4 grid. The dot is one column wide and two cells tall. +const P_CELLS: [number, number][] = [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [0, 3]]; +const DOT_CELLS: [number, number][] = [[3, 2], [3, 3]]; + +function pPath(ox: number, oy: number, u: number) { + const x = (n: number) => ox + n * u; + const y = (n: number) => oy + n * u; + return `M${x(0)} ${y(0)} H${x(3)} V${y(2)} H${x(2)} V${y(3)} H${x(1)} V${y(4)} H${x(0)} Z M${x(1)} ${y(1)} V${y(2)} H${x(2)} V${y(1)} Z`; +} + +function Eye({ x, y, w = 6, h = 11 }: { x: number; y: number; w?: number; h?: number }) { + return ; +} + +/** The mark with its face, cropped tight. For lockups and small sizes. */ +function GlyphMark({ size = 48, face = true }: { size?: number; face?: boolean }) { + const u = 22; + return ( + + ); +} + +/** Peek: the counter of the P is a window, and the eyes look out through it. */ +function GlyphPeek() { + const id = useId().replace(/:/g, ''); + const u = 24; + const ox = 18; + const oy = 36; + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +/** Carry: the dot rides on the P's head. */ +function GlyphCarry() { + const u = 22; + const ox = 36; + const oy = 58; + return ( + + + + + + + + + + + + + + + + ); +} + +/** Walk: the pair on the road. The ground moves while the run is going. */ +function GlyphWalk() { + const u = 20; + const ox = 58; + const oy = 52; + return ( + + + {Array.from({ length: 18 }, (_, i) => ( + + ))} + + + + + + + + + + + + + + + ); +} + +// Where each cell flies to while the mark takes itself apart, and where it lands in a heap. +const SCATTER = [ + [-30, -26, -35], [-6, -40, 20], [24, -30, 40], [-40, 4, -20], [36, -6, 25], + [-26, 30, 15], [10, 26, -30], [-14, 44, 45], [30, 18, -15], [22, 36, 30], +]; +const PILE = [ + [16, 118, 0], [40, 118, 6], [64, 118, -4], [88, 118, 3], [112, 118, -8], + [134, 120, 12], [28, 96, -10], [52, 96, 4], [76, 97, -6], [100, 95, 15], +]; + +/** Assemble: the mark built from its own grid. It comes apart while it thinks. */ +function GlyphAssemble() { + const u = 22; + const ox = 36; + const oy = 40; + const cells = [...P_CELLS, ...DOT_CELLS]; + return ( + + + + + {cells.map(([cx, cy], i) => { + const x = ox + cx * u; + const y = oy + cy * u; + const [sx, sy, sr] = SCATTER[i]; + const [px, py, pr] = PILE[i]; + const isDot = i >= P_CELLS.length; + return ( + + ); + })} + + + + + + + ); +} + +const LAYERS = 12; + +/** Extrude: the mark as a solid, stacked from flat layers. */ +function GlyphExtrude() { + const u = 22; + return ( +
+
+ {Array.from({ length: LAYERS }, (_, i) => ( + + ))} +
+ +
+ ); +} + +/** The wordmark, with the glyph beside it and the square dot as the tittle on the i. */ +function GlyphWordmark() { + return ( +
+ +
+ ); +} + +// Pixel glyph: 11 × 10 pixels packed into half blocks, two pixel rows per character. +type Eyes = 'center' | 'left' | 'right' | 'shut' | 'wide'; +type PixelSpec = { eyes: Eyes; dot: number | 'down' }; + +const EYE_COLS: Record = { center: [1, 4], left: [0, 3], right: [2, 5], shut: [], wide: [1, 4] }; + +function pixelGrid({ eyes, dot }: PixelSpec) { + const W = 11; + const H = 10; + const grid: ('p' | 'd' | null)[][] = Array.from({ length: H }, () => Array<'p' | 'd' | null>(W).fill(null)); + for (const [cx, cy] of P_CELLS) { + for (let dy = 0; dy < 2; dy++) for (let dx = 0; dx < 2; dx++) grid[2 + cy * 2 + dy][cx * 2 + dx] = 'p'; + } + for (const col of EYE_COLS[eyes]) { + grid[3][col] = null; + if (eyes === 'wide') grid[2][col] = null; + } + if (dot === 'down') { + for (let r = 8; r < 10; r++) for (let c = 7; c < 11; c++) grid[r][c] = 'd'; + } else { + for (let r = 6 + dot; r < 10 + dot; r++) for (let c = 6; c < 8; c++) grid[r][c] = 'd'; + } + const rows: { ch: string; dot: boolean }[][] = []; + for (let r = 0; r < H; r += 2) { + rows.push( + grid[r].map((top, c) => { + const bottom = grid[r + 1][c]; + const ch = top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' '; + return { ch, dot: top === 'd' || bottom === 'd' }; + }), + ); + } + return rows; +} + +const GLYPH_PIXEL: Record = { + idle: [ + { eyes: 'center', dot: 0 }, + { eyes: 'center', dot: 0 }, + { eyes: 'left', dot: 0 }, + { eyes: 'center', dot: 0 }, + { eyes: 'right', dot: 0 }, + { eyes: 'shut', dot: 0 }, + ], + thinking: [ + { eyes: 'right', dot: 0 }, + { eyes: 'right', dot: -1 }, + { eyes: 'right', dot: -2 }, + { eyes: 'right', dot: -1 }, + ], + input: [ + { eyes: 'wide', dot: -2 }, + { eyes: 'wide', dot: -2 }, + { eyes: 'wide', dot: 0 }, + ], + blocked: [ + { eyes: 'right', dot: 'down' }, + { eyes: 'right', dot: 'down' }, + { eyes: 'shut', dot: 'down' }, + ], + done: [ + { eyes: 'shut', dot: -2 }, + { eyes: 'shut', dot: -1 }, + { eyes: 'shut', dot: 0 }, + { eyes: 'shut', dot: 0 }, + ], +}; + +/** The glyph in block characters, for the terminal. The dot takes the state colour. */ +function GlyphPixel({ state }: { state: MascotState }) { + const reduced = usePrefersReducedMotion(); + const [frame, setFrame] = useState(0); + useEffect(() => { + setFrame(0); + if (reduced) return; + const timer = window.setInterval(() => setFrame((n) => n + 1), state === 'idle' ? 650 : 240); + return () => window.clearInterval(timer); + }, [state, reduced]); + const frames = GLYPH_PIXEL[state]; + const rows = pixelGrid(frames[frame % frames.length]); + return ( +
+      {rows.map((row, r) => (
+        
+          {row.map((cell, c) => (
+            
+              {cell.ch}
+            
+          ))}
+          {r < rows.length - 1 ? '\n' : null}
+        
+      ))}
+    
+ ); +} + +export { GlyphAssemble, GlyphCarry, GlyphExtrude, GlyphMark, GlyphPeek, GlyphPixel, GlyphWalk, GlyphWordmark }; diff --git a/src/renderer/components/LiveMascots.tsx b/src/renderer/components/LiveMascots.tsx new file mode 100644 index 0000000..d83bd34 --- /dev/null +++ b/src/renderer/components/LiveMascots.tsx @@ -0,0 +1,436 @@ +import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'; +import { PiMark } from './PiMark'; + +type MascotState = 'idle' | 'thinking' | 'input' | 'blocked' | 'done'; +type MascotTone = 'blue' | 'accent' | 'warm' | 'sage' | 'rust'; +type MascotGear = 'none' | 'antenna' | 'halo' | 'bead'; + +const PI_D = + 'M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z'; +const PI_DOT_D = 'M517.36 400H634.72V634.72H517.36Z'; +const PI_SPAN = 634.72 - 165.29; +const RX = 0.8660254037844386; + +// Isometric cube, top-face centre at (cx, cy), edge e. Same geometry as build-identity.py. +function cubeFaces(cx: number, cy: number, e: number) { + const dx = e * RX; + const dy = e / 2; + const pts = (list: [number, number][]) => list.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`).join(' '); + return { + top: pts([[cx, cy - dy], [cx + dx, cy], [cx, cy + dy], [cx - dx, cy]]), + right: pts([[cx + dx, cy], [cx, cy + dy], [cx, cy + dy + e], [cx + dx, cy + e]]), + left: pts([[cx - dx, cy], [cx, cy + dy], [cx, cy + dy + e], [cx - dx, cy + e]]), + // Maps (along, down) on the right face, origin at its front-top corner. + rightFace: `matrix(${RX} -0.5 0 1 ${cx.toFixed(2)} ${(cy + dy).toFixed(2)})`, + leftFace: `matrix(${-RX} -0.5 0 1 ${cx.toFixed(2)} ${(cy + dy).toFixed(2)})`, + topFace: (() => { + const b = dy / PI_SPAN; + const ty = cy - dy - 2 * b * 165.29; + return `matrix(${(dx / PI_SPAN).toFixed(6)} ${b.toFixed(6)} ${(-dx / PI_SPAN).toFixed(6)} ${b.toFixed(6)} ${cx.toFixed(2)} ${ty.toFixed(2)})`; + })(), + }; +} + +function Cube({ cx, cy, e, pi = false, children }: { cx: number; cy: number; e: number; pi?: boolean; children?: ReactNode }) { + const f = cubeFaces(cx, cy, e); + return ( + <> + + + + {pi ? ( + + + + + ) : null} + {children} + + ); +} + +// Writes the pointer position into --look-x / --look-y, each -1..1, on the stage element. +function usePointerLook() { + const ref = useRef(null); + useEffect(() => { + const node = ref.current; + if (!node) return; + const move = (event: PointerEvent) => { + const box = node.getBoundingClientRect(); + const x = ((event.clientX - box.left) / box.width) * 2 - 1; + const y = ((event.clientY - box.top) / box.height) * 2 - 1; + node.style.setProperty('--look-x', Math.max(-1, Math.min(1, x)).toFixed(3)); + node.style.setProperty('--look-y', Math.max(-1, Math.min(1, y)).toFixed(3)); + }; + const leave = () => { + node.style.setProperty('--look-x', '0'); + node.style.setProperty('--look-y', '0'); + }; + window.addEventListener('pointermove', move); + node.addEventListener('pointerleave', leave); + return () => { + window.removeEventListener('pointermove', move); + node.removeEventListener('pointerleave', leave); + }; + }, []); + return ref; +} + +function Eyes({ at, down, gap, w, h }: { at: number; down: number; gap: number; w: number; h: number }) { + return ( + + + + + ); +} + +/** Signals drawn above the head. Only the one for the current state is visible. */ +function HeadSignals({ cx, y }: { cx: number; y: number }) { + return ( + + + + + + + + + + + + + + + {[-1, 1].flatMap((sx) => + [-1, 1].map((sy) => ( + + )), + )} + + + ); +} + +function Gear({ gear, cx, top }: { gear: MascotGear; cx: number; top: number }) { + if (gear === 'antenna') { + return ( + + + + + ); + } + if (gear === 'halo') { + return ( + + + + + ); + } + if (gear === 'bead') { + return ( + + + + ); + } + return null; +} + +type BlinkProps = { + state: MascotState; + tone?: MascotTone; + gear?: MascotGear; + /** Body only, cropped tight. For avatars in lists. */ + compact?: boolean; + label?: string; +}; + +/** The cube and the dot with faces. Eyes follow the pointer, and the face and signals carry the agent state. */ +function BlinkMascot({ state, tone = 'blue', gear = 'none', compact = false, label }: BlinkProps) { + const ref = usePointerLook(); + const body = cubeFaces(110, 70, 90); + const dot = cubeFaces(226, 150, 40); + return ( +
+ +
+ ); +} + +// Text-only Blink for terminals and notifications. 7 columns, block elements only. +const PIXEL_FEET = ' ▀▘ ▝▀ '; +// Eyes and mouths are quarter- and half-cell holes, the way Clawd draws its eyes. +const PIXEL_EYES = { center: '▐█▛█▜█▌', left: '▐▛█▛██▌', right: '▐██▜█▜▌', shut: '▐█▀█▀█▌', wide: '▐█ █ █▌', flat: '▐█▄█▄█▌' }; +const PIXEL_MOUTH = { rest: '▐█████▌', smile: '▐█▙▄▟█▌', frown: '▐█▛▀▜█▌', open: '▐██▄██▌' }; + +// Six rows: signal, headroom, lid, eyes, mouth, feet. A lifted frame spends the headroom and tucks the feet. +function pixelFrame(signal: string, eyes: string, mouth: string, lift = false, shift = 0) { + const blank = ' '; + const rows = lift + ? [signal, '▗▄▄▄▄▄▖', eyes, mouth, ' ▘ ▝ ', blank] + : [signal, blank, '▗▄▄▄▄▄▖', eyes, mouth, PIXEL_FEET]; + const pad = (row: string) => (shift > 0 ? ` ${row.slice(0, -1)}` : shift < 0 ? `${row.slice(1)} ` : row); + return rows.map(pad).join('\n'); +} + +const PIXEL_FRAMES: Record = { + idle: [ + pixelFrame(' ', PIXEL_EYES.center, PIXEL_MOUTH.rest), + pixelFrame(' ', PIXEL_EYES.center, PIXEL_MOUTH.rest), + pixelFrame(' ', PIXEL_EYES.left, PIXEL_MOUTH.rest), + pixelFrame(' ', PIXEL_EYES.center, PIXEL_MOUTH.rest), + pixelFrame(' ', PIXEL_EYES.right, PIXEL_MOUTH.rest), + pixelFrame(' ', PIXEL_EYES.shut, PIXEL_MOUTH.rest), + ], + thinking: [ + pixelFrame(' ▪ ', PIXEL_EYES.right, PIXEL_MOUTH.rest), + pixelFrame(' ▪ ▪ ', PIXEL_EYES.right, PIXEL_MOUTH.rest), + pixelFrame(' ▪ ▪ ▪ ', PIXEL_EYES.right, PIXEL_MOUTH.rest), + pixelFrame(' ', PIXEL_EYES.right, PIXEL_MOUTH.rest), + ], + input: [ + pixelFrame(' ▮ ', PIXEL_EYES.wide, PIXEL_MOUTH.open), + pixelFrame(' ▮ ', PIXEL_EYES.wide, PIXEL_MOUTH.open, true), + pixelFrame(' ', PIXEL_EYES.wide, PIXEL_MOUTH.open), + ], + blocked: [ + pixelFrame(' ✕ ', PIXEL_EYES.flat, PIXEL_MOUTH.frown), + pixelFrame(' ✕ ', PIXEL_EYES.flat, PIXEL_MOUTH.frown, false, 1), + pixelFrame(' ✕ ', PIXEL_EYES.flat, PIXEL_MOUTH.frown, false, -1), + pixelFrame(' ✕ ', PIXEL_EYES.flat, PIXEL_MOUTH.frown), + pixelFrame(' ✕ ', PIXEL_EYES.flat, PIXEL_MOUTH.frown), + ], + done: [ + pixelFrame(' ', PIXEL_EYES.shut, PIXEL_MOUTH.smile), + pixelFrame('▪ ▪', PIXEL_EYES.shut, PIXEL_MOUTH.smile, true), + pixelFrame(' ▪ ▪ ', PIXEL_EYES.shut, PIXEL_MOUTH.smile), + pixelFrame(' ', PIXEL_EYES.center, PIXEL_MOUTH.smile), + ], +}; + +function usePrefersReducedMotion() { + const [reduced, setReduced] = useState(false); + useEffect(() => { + const query = window.matchMedia('(prefers-reduced-motion: reduce)'); + setReduced(query.matches); + const change = () => setReduced(query.matches); + query.addEventListener('change', change); + return () => query.removeEventListener('change', change); + }, []); + return reduced; +} + +type PixelSkin = 'mono' | 'terminal' | 'cube' | 'crest' | 'blueprint' | 'paper'; + +// The Pi mark on its 4×4 grid, packed two pixel rows per character with half blocks. +// ■■■· █▀█· +// ■·■· +// ■■·■ █▀·█ +// ■··■ +const PIXEL_PI = [' █▀█ ', ' █▀ █ ']; +const PIXEL_LID = '▗▄▄▄▄▄▖'; + +type PixelRow = { kind: 'signal' | 'pi' | 'lid' | 'body' | 'feet' | 'air'; text: string; shift: number }; + +function pixelRows(frame: string, skin: PixelSkin): PixelRow[] { + const lines = frame.split('\n'); + const lidAt = lines.findIndex((line) => line.includes('▄▄▄▄▄')); + const lid = lines[lidAt]; + const shift = lid.startsWith(' ') ? 1 : lid.startsWith(PIXEL_LID[0]) ? 0 : -1; + const rows: PixelRow[] = lines.map((text, i) => { + let kind: PixelRow['kind'] = 'body'; + if (i === 0) kind = 'signal'; + else if (i === lidAt) kind = 'lid'; + else if (i < lidAt || !text.trim()) kind = 'air'; + else if (i > lidAt + 2) kind = 'feet'; + return { kind, text, shift }; + }); + if (skin !== 'crest') return rows; + const pad = (row: string) => (shift > 0 ? ` ${row.slice(0, -1)}` : shift < 0 ? `${row.slice(1)} ` : row); + return [rows[0], ...PIXEL_PI.map((text) => ({ kind: 'pi' as const, text: pad(text), shift })), ...rows.slice(1)]; +} + +// The cube skin lights the right half of the body and shades the left, like the emblem faces. +function PixelCells({ row }: { row: PixelRow }) { + return ( + <> + {[...row.text].map((ch, i) => { + const col = i - row.shift; + const face = col < 3 ? 'l' : col > 3 ? 'r' : 'm'; + return ( + + {ch} + + ); + })} + + ); +} + +/** Blink drawn in block characters. Loops its state until the state changes. */ +function PixelMascot({ state, skin = 'mono' }: { state: MascotState; skin?: PixelSkin }) { + const reduced = usePrefersReducedMotion(); + const [frame, setFrame] = useState(0); + useEffect(() => { + setFrame(0); + if (reduced) return; + const timer = window.setInterval(() => setFrame((n) => n + 1), state === 'idle' ? 700 : 260); + return () => window.clearInterval(timer); + }, [state, reduced]); + const frames = PIXEL_FRAMES[state]; + const rows = pixelRows(frames[frame % frames.length], skin); + return ( +
+      {rows.map((row, i) => (
+        
+          {skin === 'cube' && row.kind === 'body' ?  : row.text}
+          {i < rows.length - 1 ? '\n' : null}
+        
+      ))}
+    
+ ); +} + +const PROMPT_LINES = [34, 58, 22, 46, 64, 30]; +const STEAM = ['{', 'π', '}', '›', ';', '_']; + +/** The code mascot. The right face is a terminal that types while the agent works. */ +function PromptMascot({ state }: { state: MascotState }) { + const ref = usePointerLook(); + const f = cubeFaces(140, 90, 110); + return ( +
+ +
+ ); +} + +/** A real 3D cube. It turns at rest, unfolds into its net while thinking, and snaps shut when done. */ +function FoldMascot({ state }: { state: MascotState }) { + const ref = usePointerLook(); + return ( +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + +
+ ); +} + +export { BlinkMascot, FoldMascot, PixelMascot, PromptMascot, usePrefersReducedMotion }; +export type { MascotGear, MascotState, MascotTone, PixelSkin }; diff --git a/src/renderer/components/PiSet.tsx b/src/renderer/components/PiSet.tsx new file mode 100644 index 0000000..373d5e9 --- /dev/null +++ b/src/renderer/components/PiSet.tsx @@ -0,0 +1,241 @@ +import { useId, type CSSProperties, type ReactNode } from 'react'; +import type { MascotState } from './LiveMascots'; +import { PiMark } from './PiMark'; + +/** + * The pi.dev set: mascots that are not cubes, and the small assets around them. + * Every piece reads the same four inks from its look, and the state colour lands on one part only. + */ +type PiLook = 'ink' | 'blueprint' | 'paper' | 'warm'; +type PiShape = 'pip' | 'brackets' | 'caret' | 'glyph' | 'stamp'; + +type SetProps = { look: PiLook; state?: MascotState; children: ReactNode; className?: string }; + +/** Sets the look and state for everything inside it. */ +function PiSurface({ look, state = 'idle', children, className }: SetProps) { + return ( +
+ {children} +
+ ); +} + +function Face({ cx, cy, gap = 24, w = 8, h = 14, mouth = 22 }: { cx: number; cy: number; gap?: number; w?: number; h?: number; mouth?: number }) { + const my = cy + mouth; + return ( + + + + + + {mouth > 0 ? ( + <> + + + + + + ) : null} + + ); +} + +function Shadow({ rx = 40 }: { rx?: number }) { + return ; +} + +/** Pip: Pi is a circle, so the first mascot is one. It rolls when a run lands. */ +function Pip() { + return ( + <> + + + + + + + + + + + ); +} + +const CORNERS = [ + { id: 'tl', d: 'M46 66 V44 H68', dx: -1, dy: -1 }, + { id: 'tr', d: 'M92 44 H114 V66', dx: 1, dy: -1 }, + { id: 'bl', d: 'M46 96 V118 H68', dx: -1, dy: 1 }, + { id: 'br', d: 'M114 96 V118 H92', dx: 1, dy: 1 }, +]; + +/** Brackets: the figure-frame corners, with a face floating inside. */ +function Brackets() { + return ( + <> + + + + + {CORNERS.map((c) => ( + + ))} + + + + + ); +} + +/** Caret: the block cursor. It blinks at rest and leaves a line of type behind it while it works. */ +function Caret() { + return ( + <> + + + {[0, 1, 2, 3].map((i) => ( + + ))} + + + + + + + ); +} + +/** Glyph: the Pi mark stood up. The P carries the face and the dot is its companion. */ +function Glyph() { + const u = 22; + const ox = 22; + const oy = 38; + const p = (x: number, y: number) => `${ox + x * u} ${oy + y * u}`; + const d = `M${p(0, 0)} H${ox + 3 * u} V${oy + 2 * u} H${ox + 2 * u} V${oy + 3 * u} H${ox + u} V${oy + 4 * u} H${ox} Z M${p(1, 1)} V${oy + 2 * u} H${ox + 2 * u} V${oy + u} Z`; + return ( + <> + + + + + + + + + + + + + + + + + ); +} + +/** Stamp: the seal, with the ring of type turning around a face. */ +function Stamp() { + const id = useId().replace(/:/g, ''); + return ( + <> + + + + + + + + PIDEX · AGENT · PIDEX · AGENT · + + + + + + ); +} + +const SHAPES: Record ReactNode> = { pip: Pip, brackets: Brackets, caret: Caret, glyph: Glyph, stamp: Stamp }; + +function PiMascot({ shape, label }: { shape: PiShape; label?: string }) { + const Shape = SHAPES[shape]; + return ( + + + + ); +} + +type StampTone = 'idle' | 'thinking' | 'input' | 'blocked' | 'done'; + +/** A rubber stamp in Departure Mono, set slightly crooked. */ +function PiStamp({ tone = 'idle', tilt = -3, children }: { tone?: StampTone; tilt?: number; children: ReactNode }) { + return ( + + {children} + + ); +} + +type RuleKind = 'ruler' | 'dotted' | 'mark' | 'thread'; + +function PiRule({ kind }: { kind: RuleKind }) { + if (kind === 'mark') { + return ( +
+ + + +
+ ); + } + return
; +} + +type PiIconName = 'session' | 'terminal' | 'branch' | 'diff' | 'file' | 'check' | 'stop' | 'loop' | 'spark' | 'pi'; + +const ICONS: Record, ReactNode> = { + session: ( + <> + + + + ), + terminal: ( + <> + + + + ), + branch: ( + <> + + + + ), + diff: , + file: , + check: , + stop: , + loop: , + spark: , +}; + +function PiIcon({ name, size = 20 }: { name: PiIconName; size?: number }) { + if (name === 'pi') return ; + return ( + + ); +} + +export { PiIcon, PiMascot, PiRule, PiStamp, PiSurface }; +export type { PiIconName, PiLook, PiShape, RuleKind, StampTone }; diff --git a/src/renderer/components/Round.tsx b/src/renderer/components/Round.tsx new file mode 100644 index 0000000..4ba218d --- /dev/null +++ b/src/renderer/components/Round.tsx @@ -0,0 +1,47 @@ +import { PiMark } from './PiMark'; + +function Orb() { + return ( +
+
+
+
+ +
+
+
+ + orb + +
+ ); +} + +function Hoop() { + return ( +
+
+
+
+
+
+ + hoop + +
+ ); +} + +function Ring() { + return ( + + + loading + + ); +} + +export { Hoop, Orb, Ring }; diff --git a/src/renderer/components/Schematic.tsx b/src/renderer/components/Schematic.tsx new file mode 100644 index 0000000..d35d30b --- /dev/null +++ b/src/renderer/components/Schematic.tsx @@ -0,0 +1,339 @@ +import { useId, type CSSProperties } from 'react'; +import type { MascotState } from './LiveMascots'; +import { SealStamp } from './Stamps'; + +/** + * A technical drawing of the Glyph, in the pi.dev blueprint inks: a dimensioned elevation, + * an exploded view with a parts list, the session state diagram, and a title block. + * Reads its inks from a surrounding PiSurface. + */ + +const W = 1200; +const H = 760; + +// The mark on its grid. Item numbers match the parts list. +const PARTS: { cx: number; cy: number; item: number }[] = [ + { cx: 0, cy: 0, item: 1 }, + { cx: 1, cy: 0, item: 1 }, + { cx: 2, cy: 0, item: 1 }, + { cx: 2, cy: 1, item: 2 }, + { cx: 0, cy: 1, item: 3 }, + { cx: 0, cy: 2, item: 3 }, + { cx: 0, cy: 3, item: 3 }, + { cx: 1, cy: 2, item: 4 }, + { cx: 3, cy: 2, item: 5 }, + { cx: 3, cy: 3, item: 5 }, +]; + +// Which way each item's balloon points, so it clears the other cells when the view closes up. +const BALLOON: Record = { 1: [-1, -1], 2: [1, -1], 3: [-1, 1], 4: [0, 1], 5: [1, 1] }; + +const BOM = [ + { item: 1, qty: 3, part: 'BAR', note: 'Carries the eyes' }, + { item: 2, qty: 1, part: 'JAMB', note: 'Right of the counter' }, + { item: 3, qty: 3, part: 'STEM', note: 'Left column' }, + { item: 4, qty: 1, part: 'HEEL', note: 'Closes the counter' }, + { item: 5, qty: 2, part: 'DOT', note: 'Takes the state ink' }, +]; + +type NodeId = MascotState; + +const NODES: { id: NodeId; label: string; x: number; y: number }[] = [ + { id: 'idle', label: 'IDLE', x: 50, y: 560 }, + { id: 'thinking', label: 'RUNNING', x: 256, y: 560 }, + { id: 'input', label: 'YOUR TURN', x: 440, y: 484 }, + { id: 'blocked', label: 'BLOCKED', x: 440, y: 636 }, + { id: 'done', label: 'SHIPPED', x: 620, y: 560 }, +]; + +const NODE_W = 132; +const NODE_H = 40; + +function node(id: NodeId) { + const n = NODES.find((entry) => entry.id === id)!; + return { ...n, cx: n.x + NODE_W / 2, cy: n.y + NODE_H / 2, r: n.x + NODE_W, b: n.y + NODE_H }; +} + +const EDGES: { from: NodeId; to: NodeId; d: () => string; label?: string }[] = [ + { from: 'idle', to: 'thinking', d: () => `M${node('idle').r} ${node('idle').cy} H${node('thinking').x - 4}`, label: 'prompt' }, + { from: 'thinking', to: 'input', d: () => `M${node('thinking').cx + 20} ${node('thinking').y} V${node('input').cy} H${node('input').x - 4}`, label: 'asks' }, + { from: 'input', to: 'thinking', d: () => `M${node('input').cx} ${node('input').b} V${node('thinking').y + 8} H${node('thinking').r + 4}` }, + { from: 'thinking', to: 'blocked', d: () => `M${node('thinking').cx + 20} ${node('thinking').b} V${node('blocked').cy} H${node('blocked').x - 4}`, label: 'fails' }, + { from: 'blocked', to: 'thinking', d: () => `M${node('blocked').cx} ${node('blocked').y} V${node('thinking').b - 8} H${node('thinking').r + 4}` }, + { from: 'thinking', to: 'done', d: () => `M${node('thinking').r} ${node('thinking').cy} H${node('done').x - 4}`, label: 'lands' }, + { from: 'done', to: 'idle', d: () => `M${node('done').cx} ${node('done').b} V${node('blocked').b + 22} H${node('idle').cx} V${node('idle').b + 4}`, label: 'next prompt' }, +]; + +function DimH({ x1, x2, y, from, label, marker }: { x1: number; x2: number; y: number; from: number; label: string; marker: string }) { + return ( + + + + + {label} + + + ); +} + +function DimV({ y1, y2, x, from, label, marker }: { y1: number; y2: number; x: number; from: number; label: string; marker: string }) { + const side = x < from ? -1 : 1; + return ( + + + + + {label} + + + ); +} + +function Callout({ n, x, y, lx, ly, text }: { n: number; x: number; y: number; lx: number; ly: number; text: string }) { + return ( + + + + + + {n} + + + {text} + + + ); +} + +const STATE_WORD: Record = { + idle: 'IDLE', + thinking: 'RUNNING', + input: 'YOUR TURN', + blocked: 'BLOCKED', + done: 'SHIPPED', +}; + +function GlyphSchematic({ state, date = '01 OCT 2026' }: { state: MascotState; date?: string }) { + const id = useId().replace(/:/g, ''); + const arrow = `bp-arrow-${id}`; + const arrowHot = `bp-arrow-hot-${id}`; + + // Front elevation, u = 44. + const u = 44; + const ox = 116; + const oy = 148; + const X = (n: number) => ox + n * u; + const Y = (n: number) => oy + n * u; + const pD = `M${X(0)} ${Y(0)} H${X(3)} V${Y(2)} H${X(2)} V${Y(3)} H${X(1)} V${Y(4)} H${X(0)} Z M${X(1)} ${Y(1)} V${Y(2)} H${X(2)} V${Y(1)} Z`; + + // Exploded view origin and cell size. + const ex = 690; + const ey = 110; + const s = 38; + + const zonesX = [1, 2, 3, 4, 5, 6]; + const zonesY = ['A', 'B', 'C', 'D']; + + return ( + + + + + + + + + + + {/* Sheet border with zone references */} + + + {zonesX.map((z, i) => { + const x = 30 + ((W - 60) / 6) * i; + return ( + + {i > 0 ? : null} + {z} + {z} + + ); + })} + {zonesY.map((z, i) => { + const y = 30 + ((H - 60) / 4) * i; + return ( + + {i > 0 ? : null} + {z} + {z} + + ); + })} + + {/* Front elevation */} + + {[0, 1, 2, 3, 4].map((n) => ( + + ))} + {[0, 1, 2, 3, 4].map((n) => ( + + ))} + + + + + + + + + + + + + + + + + + + FRONT ELEVATION · 4:1 + + + + + {/* Exploded view */} + + {PARTS.map((part, i) => { + const first = PARTS.findIndex((p) => p.item === part.item) === i; + const x = ex + part.cx * s; + const y = ey + part.cy * s + (part.item === 5 ? 12 : 0); + return ( + + + + + {first ? (() => { + const [bx, by] = BALLOON[part.item]; + const px = x + s / 2 + bx * (s / 2 - 6); + const py = y + s / 2 + by * (s / 2 - 6); + const cx = x + s / 2 + bx * (s / 2 + 34) + (bx === 0 ? 0 : 0); + const cy = y + s / 2 + by * (s / 2 + 34); + return ( + <> + + + + + {part.item} + + + ); + })() : null} + + ); + })} + + EXPLODED VIEW · 10 PARTS + + + + + {/* Parts list */} + + + + {['ITEM', 'QTY', 'PART', 'NOTE'].map((h, i) => ( + + {h} + + ))} + {BOM.map((row, r) => ( + + {r > 0 ? : null} + {row.item} + {row.qty} + {row.part} + {row.note} + + ))} + + + {/* State diagram */} + + {EDGES.map((edge) => { + const hot = edge.from === state; + const d = edge.d(); + return ( + + + + ); + })} + {EDGES.filter((e) => e.label).map((edge) => { + const a = node(edge.from); + const b = node(edge.to); + const lx = edge.to === 'idle' ? (a.cx + b.cx) / 2 : edge.from === 'idle' || edge.to === 'done' ? (a.r + b.x) / 2 : a.cx + 30; + const ly = edge.to === 'idle' ? node('blocked').b + 16 : edge.to === 'input' ? b.cy - 8 : edge.to === 'blocked' ? b.cy - 8 : edge.to === 'done' ? a.cy + 18 : a.cy - 8; + return ( + + {edge.label} + + ); + })} + {NODES.map((n) => ( + + + + {n.label} + + + ))} + + STATE SCHEMA · ONE SESSION + + + + + {/* Title block */} + + + + + pidex · Glyph + + + DWG PDX-001 + + {[ + ['SCALE', '4 : 1'], + ['UNIT', 'u = 44'], + ['REV', 'A'], + ['SHEET', '1 / 1'], + ['DATE', date], + ['DRAWN', 'pidex'], + ['CHECKED', 'you'], + ['STATE', STATE_WORD[state]], + ['LOOK', 'pi.dev'], + ].map(([k, v], i) => { + const col = i % 3; + const row = Math.floor(i / 3); + return ( + + + {k} + + + {v} + + + ); + })} + + + + + + + ); +} + +export { GlyphSchematic }; diff --git a/src/renderer/components/Stamps.tsx b/src/renderer/components/Stamps.tsx new file mode 100644 index 0000000..8d1b489 --- /dev/null +++ b/src/renderer/components/Stamps.tsx @@ -0,0 +1,252 @@ +import { useId, type CSSProperties, type ReactNode } from 'react'; +import type { MascotState } from './LiveMascots'; + +/** + * Stamps: rubber stamps, seals, tickets, postmarks, and stickers in the pi.dev inks. + * A tone picks the ink. Inside a PiSurface the paper look darkens each ink so it holds on moonstone. + */ +type StampTone = MascotState; + +const STAMP_WORD: Record = { + idle: 'draft', + thinking: 'running', + input: 'your turn', + blocked: 'blocked', + done: 'shipped', +}; + +type StampBase = { tone?: StampTone; tilt?: number; inked?: boolean }; + +function stampProps({ tone = 'done', tilt = 0, inked = false }: StampBase, className: string) { + return { + className: `ds-st ${className}`, + 'data-tone': tone, + 'data-inked': inked ? 'true' : undefined, + style: { rotate: `${tilt}deg` } as CSSProperties, + }; +} + +/** + * Worn rubber: speckles the ink and roughens the edge. Render once per page; stamps with + * inked set point at it. + */ +function InkDefs() { + return ( + + ); +} + +// The Glyph cut from the ink: the P and the dot, with the eyes left as holes. +function InkGlyph({ x, y, u }: { x: number; y: number; u: number }) { + const X = (n: number) => x + n * u; + const Y = (n: number) => y + n * u; + const p = `M${X(0)} ${Y(0)} H${X(3)} V${Y(2)} H${X(2)} V${Y(3)} H${X(1)} V${Y(4)} H${X(0)} Z M${X(1)} ${Y(1)} V${Y(2)} H${X(2)} V${Y(1)} Z`; + const eye = u * 0.26; + const eyeH = u * 0.5; + const eyes = `M${X(0.4)} ${Y(0.22)} h${eye} v${eyeH} h${-eye} Z M${X(2.34)} ${Y(0.22)} h${eye} v${eyeH} h${-eye} Z`; + return ( + + + + + ); +} + +/** Label: the double-bordered word. */ +function LabelStamp({ children, ...base }: StampBase & { children?: ReactNode }) { + return {children ?? STAMP_WORD[base.tone ?? 'done']}; +} + +/** Seal: a ringed stamp with the Glyph in the middle and the state across a band. */ +function SealStamp({ text = 'PIDEX · SESSIONS · 01 OCT 2026 ·', size, ...base }: StampBase & { text?: string; size?: number }) { + const id = useId().replace(/:/g, ''); + const word = STAMP_WORD[base.tone ?? 'done']; + return ( + // size sets real width and height attributes, which a seal nested inside another SVG needs. + + + + + + {text} + + + + + {word.toUpperCase()} + + + ); +} + +/** Ticket: ink ground, notched sides, a stub with the Glyph and a number. */ +function TicketStamp({ session, number, ...base }: StampBase & { session: string; number: number }) { + const word = STAMP_WORD[base.tone ?? 'done']; + return ( +
+
+ + № {String(number).padStart(3, '0')} +
+
+ pidex · admit one run + {session} + {word} +
+
+ ); +} + +/** Postmark: a dated circle with cancellation waves running off to the right. */ +function PostmarkStamp({ date = '01 OCT 26', place = '~/PROJECTS', ...base }: StampBase & { date?: string; place?: string }) { + const id = useId().replace(/:/g, ''); + const waves = [30, 44, 58, 72, 86]; + return ( + + + + + + + + PIDEX · SESSIONS + + + + + {place} + + + + {date} + + {waves.map((y) => ( + + ))} + + ); +} + +/** Review: a small form stamp. The result is ringed by hand. */ +function ReviewStamp({ session, turns, by = 'you', ...base }: StampBase & { session: string; turns: number; by?: string }) { + const word = STAMP_WORD[base.tone ?? 'done']; + return ( +
+
pidex · review
+
+
session
+
{session}
+
turns
+
{turns}
+
by
+
{by}
+
result
+
+ {word} + +
+
+
+ ); +} + +type StickerKind = 'glyph' | 'pi' | 'built' | 'ship'; + +/** Stickers: die-cut, with a moonstone edge and a lift shadow. Fixed brand inks, no tone. */ +function Sticker({ kind, tilt = 0 }: { kind: StickerKind; tilt?: number }) { + const ringId = `pi-ring-${useId().replace(/:/g, '')}`; + const style = { rotate: `${tilt}deg` } as CSSProperties; + if (kind === 'glyph') { + return ( + + + + + + + + + + ); + } + if (kind === 'pi') { + return ( + + + + + + 3.14159 26535 89793 23846 · + + + π + + + ); + } + if (kind === 'built') { + return ( + + + + + built with pi + + + ); + } + return ( + + + + + + ship it + + + ); +} + +function InkGlyphSolid({ x, y, u }: { x: number; y: number; u: number }) { + const X = (n: number) => x + n * u; + const Y = (n: number) => y + n * u; + return ( + <> + + + + ); +} + +/** Slams its child down whenever pressKey changes, with a ring of ink spreading out. */ +function StampPress({ pressKey, children }: { pressKey: string; children: ReactNode }) { + return ( +
+
+ ); +} + +export { InkDefs, LabelStamp, PostmarkStamp, ReviewStamp, SealStamp, StampPress, Sticker, TicketStamp, STAMP_WORD }; +export type { StickerKind, StampTone }; diff --git a/src/renderer/demo.css b/src/renderer/demo.css index 5d6ed73..a2d442a 100644 --- a/src/renderer/demo.css +++ b/src/renderer/demo.css @@ -66,10 +66,17 @@ body { gap: var(--space-4); } -.style-grid > * { +.style-grid > *, +.round-grid > * { min-width: 0; } +.round-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-4); +} + .mascot-grid { display: grid; grid-template-columns: 1.15fr 0.85fr; @@ -261,7 +268,527 @@ body { } .mast h1 { font-size: 2.6rem; } .cube-mark { height: 360px; } + .round-grid { grid-template-columns: 1fr 1fr; } .mascot-grid, .style-grid--lockup, .pattern-split { grid-template-columns: 1fr; } } + +.live-mascots { + display: grid; + gap: var(--space-4); + margin-top: var(--space-6); +} + +.live-mascots-head { + display: flex; + flex-wrap: wrap; + align-items: end; + justify-content: space-between; + gap: var(--space-4); +} + +.live-mascots-head h3 { margin: 0; } + +.live-mascots-switch { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); +} + +.live-mascots .stage { + overflow: hidden; + min-height: 340px; +} + +@media (max-width: 860px) { + .live-mascots .object-grid { grid-template-columns: 1fr; } + .live-mascots .ds-live--fold { --s: 80px; } +} + +.live-block { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-4); + margin-top: var(--space-5); +} + +.live-block h3 { margin: 0; } + +.live-note { + max-width: 60ch; + margin: var(--space-2) 0 0; + color: var(--copy); +} + +.variant-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-4); +} + +.stage--variant { + min-height: 220px; + --avatar: 128px; +} + +.roster { + display: grid; + margin: 0; + padding: 0; + list-style: none; + border: 1px solid var(--line); + background: var(--bg-deep); +} + +.roster-row { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: var(--space-4); + padding: var(--space-3) var(--space-4); + --avatar: 40px; +} + +.roster-row + .roster-row { border-top: 1px solid var(--line); } + +.roster-text { + display: grid; + gap: 2px; + min-width: 0; +} + +.roster-name { font-size: 1.05rem; } + +.roster-meta { + overflow: hidden; + font-family: var(--accent-mono); + font-size: 0.72rem; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; +} + +.pixel-term { + display: flex; + align-items: center; + gap: var(--space-5); + padding: var(--space-5); + background: #0d1116; +} + +.pixel-term-text { + display: grid; + gap: 4px; + font-family: var(--mono); + font-size: 0.85rem; + color: var(--muted); +} + +.pixel-term-title { + font-family: var(--accent-mono); + font-size: 1rem; + color: var(--text); +} + +.pixel-term-state { color: var(--moonstone); } +.pixel-term-state[data-state='thinking'] { color: var(--success); } +.pixel-term-state[data-state='input'] { color: var(--sunkissed); } +.pixel-term-state[data-state='blocked'] { color: var(--terracotta-light); } +.pixel-term-state[data-state='done'] { color: var(--accent); } + +.pixel-row { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: var(--space-4); +} + +.pixel-cell { + display: grid; + justify-items: center; + gap: var(--space-3); + padding: var(--space-4); + border: 1px solid var(--line); + background: #0d1116; + font-family: var(--accent-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.pixel-cell .ds-pixel { font-size: 16px; } + +@media (max-width: 860px) { + .variant-grid { grid-template-columns: 1fr 1fr; } + .pixel-row { grid-template-columns: repeat(3, 1fr); } + .roster-row { grid-template-columns: auto 1fr; } + .roster-row .ds-badge, + .roster-row .roster-idle { grid-column: 2; justify-self: start; } +} + +.roster-idle { + font-family: var(--accent-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.skin-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--space-4); +} + +.skin-card { + display: grid; + justify-items: center; + align-content: center; + gap: var(--space-3); + min-height: 230px; + padding: var(--space-5) var(--space-4); + background: #0d1116; +} + +.skin-note { + font-family: var(--accent-mono); + font-size: 0.66rem; + text-align: center; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.skin-card--terminal { + justify-items: start; + background: #142433; + box-shadow: inset 0 0 0 1px #24405a; +} + +.skin-card--terminal .skin-note { color: #8aa3bb; } + +.skin-prompt { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-family: var(--mono); + font-size: 0.85rem; + color: #f8f8f2; +} + +.skin-card--blueprint { + position: relative; + background-color: var(--bg-canvas); + background-image: + repeating-linear-gradient(90deg, hsl(218 60% 80% / 0.16) 0 1px, transparent 1px 20px), + repeating-linear-gradient(0deg, hsl(218 60% 80% / 0.16) 0 1px, transparent 1px 20px), + repeating-linear-gradient(90deg, hsl(218 60% 80% / 0.05) 0 1px, transparent 1px 4px), + repeating-linear-gradient(0deg, hsl(218 60% 80% / 0.05) 0 1px, transparent 1px 4px); +} + +.skin-card--paper { + background: #ebe7e4; +} + +.skin-card--paper .skin-note { color: #5c5752; } + +@media (max-width: 860px) { + .skin-grid { grid-template-columns: 1fr 1fr; } +} + +.piset { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-4); +} + +.piset-switches { + display: grid; + justify-items: end; + gap: var(--space-2); +} + +.piset-row { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: var(--space-3); +} + +.piset-stage { + display: grid; + place-items: center; + min-height: 240px; + padding: var(--space-4); +} + +.piset-matrix { + display: grid; + gap: var(--space-2); +} + +.piset-matrix-row { + display: grid; + grid-template-columns: 90px repeat(5, minmax(0, 1fr)); + align-items: center; + gap: var(--space-2); +} + +.piset-matrix-label { + font-family: var(--accent-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--muted); +} + +.piset-tile { + display: grid; + place-items: center; + padding: var(--space-3); + border: 1px solid var(--line); +} + +.piset-tile .ds-pm-svg { max-width: 104px; } + +.piset-assets { + display: grid; + gap: var(--space-6); + padding: var(--space-6) var(--space-5); + border: 1px solid var(--line); +} + +.piset-stamps { + display: flex; + flex-wrap: wrap; + gap: var(--space-4); + align-items: center; +} + +.piset-rules { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-5) var(--space-6); +} + +.piset-rule, +.piset-icon { + display: grid; + gap: var(--space-2); +} + +.piset-rule span, +.piset-icon span, +.piset-empty p { + font-family: var(--accent-mono); + font-size: 0.66rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--pm-line); + opacity: 0.72; +} + +.piset-icons { + display: grid; + grid-template-columns: repeat(10, minmax(0, 1fr)); + gap: var(--space-4); +} + +.piset-icon { justify-items: center; } + +.piset-empty { + display: grid; + grid-template-columns: 160px 1fr; + align-items: center; + gap: var(--space-5); + padding: var(--space-5); + border: 1px dashed color-mix(in srgb, var(--pm-line) 45%, transparent); +} + +.piset-empty-mascot { background: transparent; } + +.piset-empty-text h4 { + margin: 0 0 var(--space-2); + font-family: var(--serif); + font-size: 1.6rem; + font-style: italic; + font-weight: 400; + color: var(--pm-line); +} + +.piset-empty-text p { margin: 0 0 var(--space-3); } + +.piset-empty-text code { + font-family: var(--mono); + font-size: 0.85rem; + color: var(--pm-line); +} + +@media (max-width: 1100px) { + .piset-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} + +@media (max-width: 860px) { + .piset-switches { justify-items: start; } + .piset-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .piset-matrix-row { grid-template-columns: 56px repeat(5, minmax(0, 1fr)); gap: 4px; } + .piset-tile { padding: 4px; } + .piset-rules { grid-template-columns: 1fr; } + .piset-icons { grid-template-columns: repeat(5, minmax(0, 1fr)); } + .piset-empty { grid-template-columns: 1fr; justify-items: center; text-align: center; } + .piset-empty-mascot { width: 120px; } +} + +.glyph-pair { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-3); +} + +.glyph-stage { + display: grid; + place-items: center; + min-height: 300px; + padding: var(--space-5); +} + +.glyph-stage--word { min-height: 300px; } + +.glyph-stage--pixel { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-6); + min-height: 220px; +} + +.glyph-stage--pixel .pixel-term-text { color: var(--pm-line); opacity: 0.85; } +.glyph-stage--pixel .pixel-term-title { color: var(--pm-line); } + +.glyph-stage--sizes { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: center; + gap: var(--space-5); + min-height: 220px; +} + +.glyph-size { + display: grid; + justify-items: center; + gap: var(--space-2); + font-family: var(--accent-mono); + font-size: 0.66rem; + color: var(--pm-line); + opacity: 0.85; +} + +@media (max-width: 860px) { + .glyph-pair { grid-template-columns: minmax(0, 1fr); } + .ds-gl-word-text { font-size: 64px; } +} + +.glyph-stage--pixel .pixel-term-state[data-state='idle'] { color: var(--pm-line); } + +.stamp-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-3); +} + +.stamp-stage { + display: grid; + place-items: center; + min-height: 280px; + padding: var(--space-5); + overflow: hidden; +} + +.stamp-inks { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + gap: var(--space-5); + padding: var(--space-6) var(--space-4); + border: 1px solid var(--line); +} + +.stamp-ink { + display: grid; + justify-items: center; + gap: var(--space-3); +} + +.stamp-ink .ds-st-seal { width: 140px; height: 140px; } + +.stamp-ink > span { + font-family: var(--accent-mono); + font-size: 0.66rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--pm-line); + opacity: 0.75; +} + +.passport-scroll { overflow-x: auto; } + +.passport { + position: relative; + min-width: 860px; + height: 560px; + border: 1px solid var(--line); + background-image: repeating-linear-gradient(0deg, color-mix(in srgb, var(--pm-line) 9%, transparent) 0 1px, transparent 1px 28px); +} + +.passport-head { + position: absolute; + top: 18px; + right: 24px; + display: grid; + justify-items: end; + gap: 2px; +} + +.passport-title { + font-family: var(--serif); + font-size: 2rem; + font-style: italic; + color: var(--pm-line); +} + +.passport-meta { + font-family: var(--accent-mono); + font-size: 0.66rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--pm-line); + opacity: 0.7; +} + +.passport-item { + position: absolute; + opacity: 0.92; +} + +.sticker-sheet { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: var(--space-6); + padding: var(--space-6); + border: 1px solid var(--line); +} + +@media (max-width: 860px) { + .stamp-row { grid-template-columns: minmax(0, 1fr); } +} + +.sheet-scroll { overflow-x: auto; } + +.sheet { + min-width: 900px; + padding: var(--space-4); + border: 1px solid var(--line); +} diff --git a/src/renderer/demo.tsx b/src/renderer/demo.tsx index 3a9e1d4..eb7a337 100644 --- a/src/renderer/demo.tsx +++ b/src/renderer/demo.tsx @@ -1,14 +1,47 @@ -import { StrictMode } from 'react'; +import { StrictMode, useEffect, useState, type ReactNode } from 'react'; import { createRoot } from 'react-dom/client'; import { AgentLoop } from './components/AgentLoop'; -import { Badge } from './components/Badge'; +import { Badge, type BadgeTone } from './components/Badge'; import { BracketButton } from './components/BracketButton'; import { CubeMark } from './components/CubeMark'; import { PanelStack, SheetTurn, TokenSpin } from './components/DeskObjects'; import { FigureFrame } from './components/FigureFrame'; import { FrameLoader } from './components/FrameLoader'; +import { + BlinkMascot, + FoldMascot, + PixelMascot, + PromptMascot, + type MascotGear, + type MascotState, + type MascotTone, + type PixelSkin, +} from './components/LiveMascots'; import { Loader } from './components/Loader'; +import { + GlyphAssemble, + GlyphCarry, + GlyphExtrude, + GlyphMark, + GlyphPeek, + GlyphPixel, + GlyphWalk, + GlyphWordmark, +} from './components/GlyphFamily'; import { PiMark } from './components/PiMark'; +import { GlyphSchematic } from './components/Schematic'; +import { + InkDefs, + LabelStamp, + PostmarkStamp, + ReviewStamp, + SealStamp, + StampPress, + Sticker, + TicketStamp, +} from './components/Stamps'; +import { PiIcon, PiMascot, PiRule, PiStamp, PiSurface, type PiIconName, type PiLook, type PiShape, type RuleKind } from './components/PiSet'; +import { Hoop, Orb, Ring } from './components/Round'; import { Skeleton } from './components/Skeleton'; import mark from '../../assets/mark.svg'; import markMono from '../../assets/mark-mono.svg'; @@ -29,6 +62,15 @@ import mascot from '../../assets/mascot.svg'; import mascotDot from '../../assets/mascot-dot.svg'; import mascotPage from '../../assets/mascot-page.svg'; import mascotDesk from '../../assets/mascot-desk.svg'; +import mascotSignal from '../../assets/mascot-signal.svg'; +import mascotHalo from '../../assets/mascot-halo.svg'; +import mascotTurn from '../../assets/mascot-turn.svg'; +import mascotTwins from '../../assets/mascot-twins.svg'; +import mascotCairn from '../../assets/mascot-cairn.svg'; +import disc from '../../assets/mark-disc.svg'; +import arc from '../../assets/mark-arc.svg'; +import orbit from '../../assets/mark-orbit.svg'; +import bead from '../../assets/mascot-bead.svg'; import './design-system/index.css'; import './demo.css'; @@ -75,6 +117,7 @@ function LogoTile({ function Demo() { return (
+

pidex · design system

@@ -121,6 +164,33 @@ function Demo() {
+
+

Round

+
+ + + + +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+

Mascot

@@ -143,6 +213,34 @@ function Demo() {
+
+ + + + + +
+ +
+ +
+

pi.dev set

+ +
+ +
+

Glyph

+ +
+ +
+

Stamps

+ +
+ +
+

Schematic

+
@@ -304,6 +402,600 @@ function Demo() { ); } +type StateMode = MascotState | 'auto'; + +/** Cycles through the states every few seconds until one is picked. */ +function useAutoState() { + const [mode, setMode] = useState('auto'); + const [tick, setTick] = useState(0); + useEffect(() => { + if (mode !== 'auto') return; + const timer = window.setInterval(() => setTick((n) => n + 1), 5200); + return () => window.clearInterval(timer); + }, [mode]); + const state = mode === 'auto' ? LIVE_STATES[tick % LIVE_STATES.length] : mode; + return { mode, setMode, state }; +} + +function StateSwitch({ mode, state, onChange }: { mode: StateMode; state: MascotState; onChange: (mode: StateMode) => void }) { + return ( +
+ {(['auto', ...LIVE_STATES] as const).map((option) => ( + onChange(option)}> + {option === 'auto' ? `auto · ${STATE_LABEL[state]}` : STATE_LABEL[option]} + + ))} +
+ ); +} + +const PI_SHAPES: { id: PiShape; caption: string }[] = [ + { id: 'pip', caption: 'Pip · Pi is a circle' }, + { id: 'brackets', caption: 'Brackets · the frame' }, + { id: 'caret', caption: 'Caret · the cursor' }, + { id: 'glyph', caption: 'Glyph · the mark, stood up' }, + { id: 'stamp', caption: 'Stamp · the seal' }, +]; + +const PI_LOOKS: { id: PiLook; label: string }[] = [ + { id: 'ink', label: 'ink' }, + { id: 'blueprint', label: 'blueprint' }, + { id: 'paper', label: 'paper' }, + { id: 'warm', label: 'warm' }, +]; + +const PI_ICONS: PiIconName[] = ['pi', 'session', 'terminal', 'branch', 'diff', 'file', 'check', 'stop', 'loop', 'spark']; +const PI_RULES: { kind: RuleKind; label: string }[] = [ + { kind: 'ruler', label: 'Ruler · 4px and 20px ticks' }, + { kind: 'mark', label: 'Mark · the Pi between two hairlines' }, + { kind: 'dotted', label: 'Dotted · 8px step' }, + { kind: 'thread', label: 'Thread · tidal fading out' }, +]; + +function LookSwitch({ look, onChange }: { look: PiLook; onChange: (look: PiLook) => void }) { + return ( +
+ {PI_LOOKS.map((option) => ( + onChange(option.id)}> + {option.label} + + ))} +
+ ); +} + +const GLYPH_POSES = [ + { id: 'glyph', caption: 'Glyph · the original', node: }, + { id: 'peek', caption: 'Peek · eyes in the counter', node: }, + { id: 'carry', caption: 'Carry · the dot rides up top', node: }, + { id: 'walk', caption: 'Walk · on the road while it runs', node: }, + { id: 'assemble', caption: 'Assemble · built from its grid', node: }, +]; + +const GLYPH_SIZES = [16, 24, 32, 48, 72, 112]; + +function GlyphFamilyDemo() { + const { mode, setMode, state } = useAutoState(); + const [look, setLook] = useState('ink'); + return ( +
+
+
+

Glyph family

+

The mark, stood up, and everything it can do.

+
+
+ + +
+
+ +
+ {GLYPH_POSES.map((pose) => ( + + + {pose.node} + + + ))} +
+ +
+ + + + + + + + + + +
+ +
+ + + +
+ pidex + ~/projects/pidex · main + + ● {STATE_LABEL[state]} + +
+
+
+ + + {GLYPH_SIZES.map((size) => ( +
+ = 24} /> + {size} +
+ ))} +
+
+
+
+ ); +} + +const PASSPORT: { node: ReactNode; left: string; top: string }[] = [ + { node: , left: '3%', top: '16%' }, + { node: , left: '24%', top: '6%' }, + { node: , left: '62%', top: '12%' }, + { node: , left: '76%', top: '34%' }, + { node: , left: '26%', top: '52%' }, + { node: your turn, left: '6%', top: '74%' }, + { node: , left: '58%', top: '68%' }, +]; + +function StampsDemo() { + const { mode, setMode, state } = useAutoState(); + const [look, setLook] = useState('ink'); + const [inked, setInked] = useState(true); + return ( +
+
+
+

Stamps

+

Seals, tickets, postmarks, and stickers.

+

+ Each stamp is pressed again whenever the state changes. Worn ink speckles the ink and + roughens its edge, like a real rubber stamp. +

+
+
+ + +
+ setInked((v) => !v)}> + worn ink · {inked ? 'on' : 'off'} + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + +
+ +
+
+

Inks

+

One ink per state.

+
+ + {LIVE_STATES.map((tone, i) => ( +
+ + {STATE_LABEL[tone]} +
+ ))} +
+
+ +
+
+

Passport

+

A page of collected stamps.

+
+
+ +
+ Passport + pidex · sessions this week +
+ {PASSPORT.map((item, i) => ( +
+ {item.node} +
+ ))} +
+
+
+ +
+
+

Stickers

+

Die-cut, for laptops and READMEs.

+
+ + + + + + +
+
+ ); +} + +const SHEET_LOOKS: { id: PiLook; label: string }[] = [ + { id: 'blueprint', label: 'blueprint' }, + { id: 'paper', label: 'whiteprint' }, + { id: 'ink', label: 'ink' }, +]; + +function SchematicDemo() { + const { mode, setMode, state } = useAutoState(); + const [look, setLook] = useState('blueprint'); + return ( +
+
+
+

Schematic

+

The Glyph, drawn up as a blueprint.

+

+ A dimensioned elevation, an exploded view with a parts list, and the session state + schema. The current state lights its box and flows out along its arrows, and the + seal is pressed onto the title block. +

+
+
+
+ {SHEET_LOOKS.map((option) => ( + setLook(option.id)}> + {option.label} + + ))} +
+ +
+
+
+ + + +
+
+ ); +} + +function PiSetDemo() { + const { mode, setMode, state } = useAutoState(); + const [look, setLook] = useState('ink'); + return ( +
+
+
+

Shapes

+

Five shapes, four looks, one set of inks.

+
+
+
+ {PI_LOOKS.map((option) => ( + setLook(option.id)}> + {option.label} + + ))} +
+ +
+
+ +
+ {PI_SHAPES.map((shape) => ( + + + + + + ))} +
+ +
+
+

Every look

+

Four looks for every shape.

+
+
+ {PI_LOOKS.map((row) => ( +
+ {row.label} + {PI_SHAPES.map((shape) => ( + + + + ))} +
+ ))} +
+
+ +
+
+

Assets

+

Stamps, rules, icons, and an empty state.

+
+ +
+ draft + running + your turn + blocked + shipped +
+
+ {PI_RULES.map((rule) => ( +
+ + {rule.label} +
+ ))} +
+
+ {PI_ICONS.map((name) => ( +
+ + {name} +
+ ))} +
+
+ + + +
+

Nothing running yet.

+

Start a session and it will show up here.

+ › pidex new "fix the flaky test" +
+
+
+
+
+ ); +} + +const LIVE_STATES: MascotState[] = ['idle', 'thinking', 'input', 'blocked', 'done']; + +const STATE_LABEL: Record = { + idle: 'idle', + thinking: 'running', + input: 'needs input', + blocked: 'blocked', + done: 'ready', +}; + +type RosterRow = { + name: string; + meta: string; + status: string; + state: MascotState; + tone: MascotTone; + gear: MascotGear; + badge?: BadgeTone; +}; + +const ROSTER: RosterRow[] = [ + { name: 'fix-auth-redirect', meta: 'pidex · turn 14', status: 'Running the auth tests', state: 'thinking', tone: 'accent', gear: 'antenna', badge: 'active' }, + { name: 'migrate-sqlite', meta: 'pidex · turn 6', status: 'Wants to drop the sessions table', state: 'input', tone: 'warm', gear: 'bead', badge: 'stale' }, + { name: 'flaky-e2e', meta: 'pidex · turn 22', status: 'Chrome would not start in CI', state: 'blocked', tone: 'rust', gear: 'none', badge: 'blocked' }, + { name: 'readme-stills', meta: 'pidex · turn 9', status: 'Four stills are ready to review', state: 'done', tone: 'sage', gear: 'halo', badge: 'done' }, + { name: 'scratch', meta: 'pidex · turn 0', status: 'Waiting for a prompt', state: 'idle', tone: 'blue', gear: 'none' }, +]; + +const VARIANTS: { tone: MascotTone; gear: MascotGear; caption: string }[] = [ + { tone: 'accent', gear: 'antenna', caption: 'Accent · antenna' }, + { tone: 'warm', gear: 'bead', caption: 'Warm · bead' }, + { tone: 'sage', gear: 'halo', caption: 'Sage · halo' }, + { tone: 'rust', gear: 'none', caption: 'Rust · bare' }, +]; + +function MascotRoster() { + return ( +
+
+

Roster

+

One Blink per session.

+

+ Each session gets a top face and a piece of gear, so you can tell them apart. The face + and the signal above the head show what the session is doing, and hold for as long as + that state lasts. +

+
+
    + {ROSTER.map((row) => ( +
  • + +
    + {row.name} + + {row.meta} · {row.status} + +
    + {row.badge ? ( + + ) : ( + {STATE_LABEL[row.state]} + )} +
  • + ))} +
+
+ ); +} + +function MascotVariants({ state }: { state: MascotState }) { + return ( +
+
+

Variants

+

Tone and gear.

+
+
+ {VARIANTS.map((v) => ( + +
+ +
+
+ ))} +
+
+ ); +} + +const PIXEL_SKINS: { id: PixelSkin; caption: string; note: string }[] = [ + { id: 'mono', caption: 'Mono', note: 'The whole body takes the state colour' }, + { id: 'terminal', caption: 'Terminal', note: 'pi.dev terminal: #142433 and #f8f8f2' }, + { id: 'cube', caption: 'Cube', note: 'Parchment lid, lit right face, tidal left' }, + { id: 'crest', caption: 'Crest', note: 'The Pi mark worn on the head' }, + { id: 'blueprint', caption: 'Blueprint', note: 'Accent linework on the page grid' }, + { id: 'paper', caption: 'Paper', note: 'Light theme: evening blue on moonstone' }, +]; + +function MascotTerminal({ state }: { state: MascotState }) { + return ( +
+
+

Terminal

+

Blink in block characters, in pi.dev colours.

+

+ For the CLI and for anywhere without SVG. Seven columns wide, in the same five states. + Each skin takes its colours from pi.dev, and the signal above the head always takes the + state colour. +

+
+ +
+ +
+ pidex + ~/projects/pidex · main + + ● {STATE_LABEL[state]} + +
+
+
+
+ {PIXEL_SKINS.map((skin) => ( + +
+ + {skin.id === 'terminal' ? ( + + › pidex + + ) : null} + {skin.note} +
+
+ ))} +
+
+ {LIVE_STATES.map((s) => ( +
+ + {STATE_LABEL[s]} +
+ ))} +
+
+ ); +} + +function LiveMascots() { + const { mode, setMode, state } = useAutoState(); + return ( +
+
+
+

Alive

+

Three takes on the pair, moving.

+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ + + +
+ ); +} + const root = document.getElementById('root'); if (root) { createRoot(root).render( diff --git a/src/renderer/design-system/components.css b/src/renderer/design-system/components.css index 5f85806..318d2cc 100644 --- a/src/renderer/design-system/components.css +++ b/src/renderer/design-system/components.css @@ -697,6 +697,76 @@ transform: rotateY(180deg) translateZ(5px); } +.ds-orb-spin, +.ds-hoop-spin { + position: relative; + width: 92px; + height: 92px; + transform-style: preserve-3d; + animation: ds-token-turn 8s linear infinite; +} + +.ds-hoop-spin { animation-name: ds-hoop-turn; } +.ds-orb-spin { animation-name: ds-orb-turn; } + +@keyframes ds-orb-turn { + from { transform: rotateX(18deg) rotateZ(0deg); } + to { transform: rotateX(18deg) rotateZ(-360deg); } +} + +@keyframes ds-hoop-turn { + from { transform: rotateX(66deg) rotateZ(0deg); } + to { transform: rotateX(66deg) rotateZ(360deg); } +} + +.ds-orb { + position: absolute; + inset: 0; + display: grid; + place-items: center; + border-radius: 50%; + background: radial-gradient(circle at 32% 28%, #4d6d8f, #1c2838 68%); + border: 1.5px solid var(--moonstone); + color: var(--moonstone); + backface-visibility: hidden; + transform: translateZ(5px); +} + +.ds-hoop { + width: 92px; + height: 92px; + border-radius: 50%; + border: 3px solid var(--moonstone); + border-top-color: var(--accent); + box-shadow: inset 0 0 0 1px #1c2838; +} + +.ds-ring { + width: 1.15rem; + height: 1.15rem; + display: block; +} + +.ds-ring-track, +.ds-ring-head { + fill: none; + stroke-width: 2.4; +} + +.ds-ring-track { stroke: var(--line); } + +.ds-ring-head { + stroke: var(--accent); + stroke-linecap: round; + stroke-dasharray: 24 46; + transform-origin: center; + animation: ds-ring-turn 1.15s linear infinite; +} + +@keyframes ds-ring-turn { + to { transform: rotate(360deg); } +} + @media (prefers-reduced-motion: reduce) { .ds-loader-ticks span, .ds-scan::after, @@ -707,7 +777,10 @@ .ds-loop-node, .ds-sheet-spin, .ds-stack-spin, - .ds-token-spin { + .ds-token-spin, + .ds-orb-spin, + .ds-hoop-spin, + .ds-ring-head { animation: none; } @@ -715,9 +788,1831 @@ .ds-sheet-spin, .ds-stack-spin { transform: rotateX(-24deg) rotateY(-32deg); } .ds-token-spin { transform: rotateX(-12deg) rotateY(-28deg); } + .ds-orb-spin { transform: rotateX(18deg) rotateZ(-24deg); } + .ds-hoop-spin { transform: rotateX(62deg) rotateZ(-20deg); } .ds-loop-node--think { color: var(--text); border-color: var(--accent); background: var(--accent-surface-soft); } } + +/* ── Live mascots ─────────────────────────────────────────────── + Three takes on the cube and the dot. Each reads data-state: + idle, thinking, or done. --look-x / --look-y (-1..1) come from the pointer. */ + +@property --open { + syntax: ''; + inherits: false; + initial-value: 0deg; +} + +@property --lid { + syntax: ''; + inherits: false; + initial-value: 90deg; +} + +.ds-live { + --look-x: 0; + --look-y: 0; + position: relative; + display: grid; + place-items: center; + width: 100%; + min-height: 300px; +} + +.ds-live svg { + width: 100%; + max-width: 300px; + height: auto; + overflow: visible; +} + +.ds-live-face { + stroke: #6e849c; + stroke-width: 1.6; + stroke-linejoin: round; +} + +.ds-live-face--top { fill: #2a3b50; } +.ds-live-face--right { fill: #3d536b; } +.ds-live-face--left { fill: #1c2838; } +.ds-live-ink { fill: var(--moonstone); } + +.ds-live-shadow { + fill: #000; + opacity: 0.34; + transform-box: fill-box; + transform-origin: center; +} + +.ds-live-body, +.ds-live-dot { + transform-box: fill-box; + transform-origin: 50% 100%; +} + +/* Blink: a face that watches the pointer */ + +.ds-live--blink .ds-live-body { + animation: ds-live-breathe 3.4s ease-in-out infinite; +} + +.ds-live--blink .ds-live-dot { + animation: ds-live-bob 2.6s ease-in-out infinite; +} + +.ds-live-eye { + fill: var(--moonstone); + transform-box: fill-box; + transform-origin: center; + animation: ds-live-blink 4.6s infinite; +} + +.ds-live-look--small .ds-live-eye { animation-delay: 0.14s; } + +.ds-live-look { + transform: translate(calc(var(--look-x) * 7px), calc(var(--look-y) * 5px)); + transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.ds-live-look--small { + transform: translate(calc(var(--look-x) * 3px), calc(var(--look-y) * 2px)); +} + +.ds-live-mouth { + fill: none; + stroke: var(--moonstone); + stroke-width: 3; + stroke-linecap: square; + stroke-linejoin: miter; + transition: opacity 160ms ease; +} + +.ds-live-mouth--smile { opacity: 0; } + +.ds-live-ticks rect { + fill: var(--moonstone); + opacity: 0; +} + +.ds-live--blink[data-state='thinking'] .ds-live-look { + transform: translate(8px, -7px); +} + +.ds-live--blink[data-state='thinking'] .ds-live-look--small { + transform: translate(3px, -3px); +} + +.ds-live--blink[data-state='thinking'] .ds-live-ticks rect { + animation: ds-live-tick 1.2s steps(1, end) infinite; +} + +.ds-live--blink[data-state='thinking'] .ds-live-ticks rect:nth-child(2) { animation-delay: 0.2s; } +.ds-live--blink[data-state='thinking'] .ds-live-ticks rect:nth-child(3) { animation-delay: 0.4s; } + +.ds-live--blink[data-state='done'] .ds-live-mouth--flat { opacity: 0; } +.ds-live--blink[data-state='done'] .ds-live-mouth--smile { opacity: 1; } + +.ds-live--blink[data-state='done'] .ds-live-body { + --hop: 44px; + animation: ds-live-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; +} + +.ds-live--blink[data-state='done'] .ds-live-dot { + --hop: 30px; + animation: ds-live-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) 0.2s infinite; +} + +.ds-live--blink[data-state='done'] .ds-live-shadow { + animation: ds-live-shadow 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; +} + +.ds-live--blink[data-state='done'] .ds-live-shadow--dot { animation-delay: 0.2s; } + +@keyframes ds-live-breathe { + 0%, 100% { transform: scale(1, 1); } + 50% { transform: scale(1.012, 0.985); } +} + +@keyframes ds-live-bob { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } +} + +@keyframes ds-live-blink { + 0%, 91%, 100% { transform: scaleY(1); } + 94% { transform: scaleY(0.08); } +} + +@keyframes ds-live-tick { + 0% { opacity: 0; transform: translateY(0); } + 30% { opacity: 1; transform: translateY(-6px); } + 70% { opacity: 0.35; transform: translateY(-10px); } + 100% { opacity: 0; } +} + +@keyframes ds-live-hop { + 0%, 92%, 100% { transform: translateY(0) scale(1, 1); } + 12% { transform: translateY(0) scale(1.08, 0.88); } + 32% { transform: translateY(calc(var(--hop) * -0.85)) scale(0.94, 1.08); } + 48% { transform: translateY(calc(var(--hop) * -1)) scale(1, 1); } + 66% { transform: translateY(0) scale(1.1, 0.86); } + 78% { transform: translateY(0) scale(0.97, 1.03); } +} + +@keyframes ds-live-shadow { + 0%, 12%, 66%, 100% { transform: scale(1); opacity: 0.34; } + 48% { transform: scale(0.62); opacity: 0.16; } +} + +/* Prompt: the code mascot */ + +.ds-live--prompt .ds-live-body { + translate: calc(var(--look-x) * 5px) 0; + transition: translate 240ms ease-out; + animation: ds-live-float 4.2s ease-in-out infinite; +} + +.ds-live-screen { + fill: #0d1116; + stroke: color-mix(in srgb, var(--accent) 45%, transparent); + stroke-width: 1; +} + +.ds-live-prompt { + fill: var(--accent); + font-family: var(--accent-mono); + font-size: 13px; +} + +.ds-live-code rect { + fill: var(--moonstone); + opacity: 0.72; + transform-box: fill-box; + transform-origin: 0 50%; + animation: ds-live-type 7.2s steps(7, end) infinite; + animation-delay: calc(var(--i) * 0.9s); + transition: opacity 200ms ease; +} + +.ds-live-code rect:nth-child(3n + 2) { fill: var(--accent); } +.ds-live-code rect:nth-child(3n) { fill: var(--sunkissed); opacity: 0.6; } + +.ds-live-caret { + fill: var(--accent); + animation: ds-live-caret 1s steps(1, end) infinite; +} + +.ds-live-status { + fill: var(--signal); + font-family: var(--accent-mono); + font-size: 13px; + opacity: 0; + transition: opacity 200ms ease; +} + +.ds-live-vents rect { + fill: #6e849c; + opacity: 0.45; +} + +.ds-live-stem { + stroke: #6e849c; + stroke-width: 2; +} + +.ds-live-bead { + color: #6e849c; + stroke: #0d1116; + stroke-width: 1; + transform-box: fill-box; + transform-origin: center; + transition: color 240ms ease; +} + +.ds-live-steam text { + fill: var(--moonstone); + font-family: var(--accent-mono); + font-size: 14px; + opacity: 0; + text-anchor: middle; +} + +.ds-live--prompt[data-state='thinking'] .ds-live-code rect { + animation-duration: 2.4s; + animation-delay: calc(var(--i) * 0.3s); +} + +.ds-live--prompt[data-state='thinking'] .ds-live-bead { + animation: ds-live-pulse 0.9s ease-in-out infinite; +} + +.ds-live--prompt[data-state='thinking'] .ds-live-steam text { + animation: ds-live-steam 2.6s ease-out infinite; + animation-delay: calc(var(--i) * 0.42s); +} + +.ds-live--prompt[data-state='input'] .ds-live-code rect, +.ds-live--prompt[data-state='blocked'] .ds-live-code rect, +.ds-live--prompt[data-state='done'] .ds-live-code rect { + animation: none; + opacity: 0.18; +} + +.ds-live--prompt:is([data-state='done'], [data-state='blocked']) .ds-live-caret { visibility: hidden; } +.ds-live--prompt[data-state='done'] .ds-live-status--done, +.ds-live--prompt[data-state='input'] .ds-live-status--input, +.ds-live--prompt[data-state='blocked'] .ds-live-status--blocked { opacity: 1; } + + +.ds-live--prompt[data-state='done'] .ds-live-body { + animation: ds-live-nod 2s cubic-bezier(0.3, 0, 0.3, 1) infinite; +} + +@keyframes ds-live-float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-6px); } +} + +@keyframes ds-live-type { + 0% { transform: scaleX(0); } + 14%, 82% { transform: scaleX(1); } + 90%, 100% { transform: scaleX(0); } +} + +@keyframes ds-live-caret { + 0% { opacity: 1; } + 50% { opacity: 0; } +} + +@keyframes ds-live-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.28); } +} + +@keyframes ds-live-steam { + 0% { opacity: 0; transform: translateY(0); } + 18% { opacity: 0.9; } + 100% { opacity: 0; transform: translateY(-46px); } +} + +@keyframes ds-live-nod { + 0%, 40%, 100% { transform: scale(1, 1); } + 10% { transform: scale(1.05, 0.92); } + 22% { transform: translateY(-10px) scale(0.98, 1.03); } +} + +/* Fold: a real cube that opens into its net */ + +.ds-live--fold { + --s: 104px; + min-height: 340px; + perspective: 900px; +} + +.ds-fold-scene, +.ds-fold-tilt, +.ds-fold-hop, +.ds-fold-spin { + width: var(--s); + height: var(--s); + transform-style: preserve-3d; +} + +.ds-fold-scene { position: relative; z-index: 1; } + +.ds-fold-tilt { + transform: rotateX(calc(-34deg - var(--look-y) * 14deg)) rotateY(calc(var(--look-x) * 30deg)); + transition: transform 500ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.ds-fold-spin { + position: relative; + animation: ds-fold-spin 16s linear infinite; +} + +.ds-fold-face { + position: absolute; + inset: 0; + box-sizing: border-box; + border: 1.5px solid #6e849c; + background: #2a3b50; + transform-style: preserve-3d; +} + +.ds-fold-bottom { + display: grid; + place-items: center; + background: #1a2432; + transform: translateY(calc(var(--s) / 2)) rotateX(90deg); +} + +.ds-fold-dot { + width: 26%; + height: 26%; + background: var(--accent); + box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 70%, transparent); +} + +.ds-fold-side { + transform-origin: 50% 100%; + transform: rotateY(calc(var(--k) * 90deg)) translateZ(calc(var(--s) / 2)) rotateX(var(--open)); +} + +.ds-fold-side--front { background: #3d536b; } +.ds-fold-side--right { background: #2f4259; } +.ds-fold-side--back { background: #24344a; } +.ds-fold-side--left { background: #1c2838; } + +.ds-fold-top { + inset: auto 0 100% 0; + height: var(--s); + display: grid; + place-items: center; + color: var(--moonstone); + background: #2a3b50; + transform-origin: 50% 100%; + transform: rotateX(var(--lid)); +} + +.ds-fold-top svg { transform: scaleX(-1); } + +.ds-fold-ground, +.ds-fold-pulse { + position: absolute; + left: 50%; + top: calc(50% + var(--s) * 0.95); + width: calc(var(--s) * 1.7); + height: calc(var(--s) * 0.34); + border-radius: 50%; + translate: -50% -50%; + pointer-events: none; +} + +.ds-fold-ground { + background: radial-gradient(closest-side, rgb(0 0 0 / 0.5), transparent); +} + +.ds-fold-pulse { + border: 2px solid var(--signal); + opacity: 0; +} + +.ds-live--fold[data-state='thinking'] .ds-fold-tilt { + transform: rotateX(calc(-58deg - var(--look-y) * 10deg)) rotateY(calc(var(--look-x) * 30deg)); +} + +.ds-live--fold[data-state='thinking'] .ds-fold-spin { animation-duration: 28s; } + +.ds-live--fold[data-state='thinking'] .ds-fold-side { + animation: ds-fold-open 4.2s cubic-bezier(0.6, 0, 0.2, 1) infinite; +} + +.ds-live--fold[data-state='thinking'] .ds-fold-side--right { animation-delay: 0.08s; } +.ds-live--fold[data-state='thinking'] .ds-fold-side--back { animation-delay: 0.16s; } +.ds-live--fold[data-state='thinking'] .ds-fold-side--left { animation-delay: 0.24s; } + +.ds-live--fold[data-state='thinking'] .ds-fold-top { + animation: ds-fold-lid 4.2s cubic-bezier(0.6, 0, 0.2, 1) 0.16s infinite; +} + +.ds-live--fold[data-state='done'] .ds-fold-hop { + animation: ds-fold-hop 1.8s cubic-bezier(0.3, 0, 0.3, 1) infinite; +} + +.ds-live--fold[data-state='done'] .ds-fold-face { border-color: color-mix(in srgb, var(--signal) 70%, #6e849c); } + +.ds-live--fold[data-state='done'] .ds-fold-pulse { + animation: ds-fold-pulse 1.8s ease-out infinite; +} + +@keyframes ds-fold-spin { + from { transform: rotateY(-38deg); } + to { transform: rotateY(322deg); } +} + +@keyframes ds-fold-open { + 0%, 96%, 100% { --open: 0deg; } + 30%, 66% { --open: -90deg; } +} + +@keyframes ds-fold-lid { + 0%, 96%, 100% { --lid: 90deg; } + 30%, 66% { --lid: 0deg; } +} + +@keyframes ds-fold-hop { + 0%, 90%, 100% { transform: translateY(0) rotateY(0); } + 10% { transform: translateY(0) scale3d(1.06, 0.9, 1.06); } + 40% { transform: translateY(-46px) rotateY(90deg); } + 64% { transform: translateY(0) rotateY(90deg) scale3d(1.08, 0.88, 1.08); } + 76% { transform: translateY(0) rotateY(90deg); } +} + +@keyframes ds-fold-pulse { + 0%, 62% { opacity: 0; scale: 0.5; } + 66% { opacity: 0.9; scale: 0.6; } + 100% { opacity: 0; scale: 1.5; } +} + +/* State colour. Signals, beads, and status text all read it. */ + +.ds-live { --signal: var(--moonstone); } +.ds-live[data-state='thinking'] { --signal: var(--success); } +.ds-live[data-state='input'] { --signal: var(--sunkissed); } +.ds-live[data-state='blocked'] { --signal: var(--terracotta-light); } +.ds-live[data-state='done'] { --signal: var(--accent); } + +.ds-live--prompt[data-state='idle'] .ds-live-bead { color: #6e849c; } +.ds-live--prompt:not([data-state='idle']) .ds-live-bead { + color: var(--signal); + filter: drop-shadow(0 0 6px var(--signal)); +} + +.ds-live--prompt[data-state='input'] .ds-live-bead { + animation: ds-live-pulse 0.6s ease-in-out infinite; +} + +.ds-live--prompt[data-state='blocked'] .ds-live-body, +.ds-live--blink[data-state='blocked'] .ds-live-body { + animation: ds-live-shake 2.2s ease-in-out infinite; +} + +/* Blink tones: the top face changes, the sides stay */ + +.ds-live[data-tone='accent'] .ds-live-face--top { fill: #4b7aa3; } +.ds-live[data-tone='warm'] .ds-live-face--top { fill: #9c7040; } +.ds-live[data-tone='sage'] .ds-live-face--top { fill: #6f7048; } +.ds-live[data-tone='rust'] .ds-live-face--top { fill: #844f3b; } + +/* Blink signals */ + +.ds-live-bang rect, +.ds-live-sparks rect { fill: var(--signal); } + +.ds-live-bang, +.ds-live-cross, +.ds-live-sparks rect { + opacity: 0; + transform-box: fill-box; + transform-origin: 50% 100%; +} + +.ds-live-cross path { + fill: none; + stroke: var(--signal); + stroke-width: 5; + stroke-linecap: square; +} + +.ds-live--blink[data-state='input'] .ds-live-bang { + opacity: 1; + animation: ds-live-call 1.4s cubic-bezier(0.3, 0, 0.3, 1) infinite; +} + +.ds-live--blink[data-state='blocked'] .ds-live-cross { opacity: 1; } + +.ds-live--blink[data-state='done'] .ds-live-sparks rect { + animation: ds-live-spark 1.7s ease-out infinite; +} + +.ds-live-mouth--frown, +.ds-live-mouth--open { opacity: 0; } + +.ds-live--blink:is([data-state='input'], [data-state='blocked']) .ds-live-mouth--flat { opacity: 0; } +.ds-live--blink[data-state='input'] .ds-live-mouth--open { opacity: 1; } +.ds-live--blink[data-state='blocked'] .ds-live-mouth--frown { opacity: 1; } + +.ds-live-eyes { + transform-box: fill-box; + transform-origin: center; + transition: transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.ds-live--blink[data-state='input'] .ds-live-look { transform: translate(-2px, 1px); } +.ds-live--blink[data-state='input'] .ds-live-look .ds-live-eyes { transform: scale(1.22); } +.ds-live--blink[data-state='blocked'] .ds-live-look { transform: translate(-3px, 5px); } +.ds-live--blink[data-state='blocked'] .ds-live-look .ds-live-eyes { transform: scaleY(0.3); } + +.ds-live--blink[data-state='input'] .ds-live-body { + --hop: 16px; + animation: ds-live-hop 1.4s cubic-bezier(0.3, 0, 0.3, 1) infinite; +} + +.ds-live--blink[data-state='input'] .ds-live-dot .ds-live-look { transform: translate(-4px, -2px); } +.ds-live--blink[data-state='blocked'] .ds-live-dot { animation: ds-live-shake 2.2s ease-in-out 0.12s infinite; } + +/* Blink gear */ + +.ds-live-gear line { + stroke: #6e849c; + stroke-width: 2.5; +} + +.ds-live-gear--antenna circle, +.ds-live-gear--bead circle { + fill: var(--signal); + stroke: #0d1116; + stroke-width: 1.5; + transition: fill 240ms ease; +} + +.ds-live-gear--halo ellipse { + fill: none; + stroke: var(--signal); + stroke-width: 2.5; + opacity: 0.85; +} + +.ds-live-halo-bead { + fill: var(--moonstone); + animation: ds-live-orbit 3.6s linear infinite; +} + +.ds-live[data-state='thinking'] .ds-live-halo-bead { animation-duration: 1.2s; } +.ds-live[data-state='blocked'] .ds-live-halo-bead { animation-play-state: paused; } + +.ds-live-gear--bead circle { + transform-box: fill-box; + transform-origin: center; + animation: ds-live-bob 2.2s ease-in-out infinite; +} + +.ds-live[data-state='thinking'] .ds-live-gear--antenna circle, +.ds-live[data-state='input'] .ds-live-gear--antenna circle { + animation: ds-live-pulse 0.8s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; +} + +/* Compact Blink for lists */ + +.ds-live--compact { + min-height: 0; + width: auto; +} + +.ds-live--compact svg { + width: var(--avatar, 44px); + max-width: none; +} + +.ds-live--compact .ds-live-look { transition: none; } + +/* Fold, input and blocked */ + +.ds-live--fold[data-state='input'] .ds-fold-hop { + animation: ds-fold-call 1.4s cubic-bezier(0.3, 0, 0.3, 1) infinite; +} + +.ds-live--fold[data-state='input'] .ds-fold-face { border-color: var(--sunkissed); } + +.ds-live--fold[data-state='blocked'] .ds-fold-spin { animation-play-state: paused; } +.ds-live--fold[data-state='blocked'] .ds-fold-hop { animation: ds-live-shake 2.2s ease-in-out infinite; } +.ds-live--fold[data-state='blocked'] .ds-fold-face { + border-color: var(--terracotta-light); + filter: saturate(0.5) brightness(0.85); +} + +/* Pixel Blink */ + +.ds-pixel { + margin: 0; + /* Menlo and SF Mono draw block elements edge to edge; the brand monos leave seams. */ + font-family: Menlo, "SF Mono", "DejaVu Sans Mono", monospace; + font-size: 22px; + line-height: 1; + letter-spacing: 0; + color: var(--moonstone); + white-space: pre; +} + +.ds-pixel[data-state='thinking'] { color: var(--success); } +.ds-pixel[data-state='input'] { color: var(--sunkissed); } +.ds-pixel[data-state='blocked'] { color: var(--terracotta-light); } +.ds-pixel[data-state='done'] { color: var(--accent); } + +/* Pixel skins. Body colour comes from the skin, the signal row from the state. */ + +.ds-pixel { --pixel-body: var(--moonstone); --pixel-signal: var(--moonstone); } +.ds-pixel[data-state='thinking'] { --pixel-signal: var(--success); } +.ds-pixel[data-state='input'] { --pixel-signal: var(--sunkissed); } +.ds-pixel[data-state='blocked'] { --pixel-signal: var(--terracotta-light); } +.ds-pixel[data-state='done'] { --pixel-signal: var(--accent); } + +.ds-pixel:not([data-skin='mono']) { color: var(--pixel-body); } +.ds-pixel-row--signal { color: var(--pixel-signal); } + +/* Terminal: pi.dev's terminal card, #142433 ground and #f8f8f2 type */ +.ds-pixel[data-skin='terminal'] { --pixel-body: #f8f8f2; } + +/* Cube: parchment lid, lit right face, tidal left face */ +.ds-pixel[data-skin='cube'] .ds-pixel-row--lid { color: var(--parchment); } +.ds-pixel[data-skin='cube'] .ds-pixel-c--l { color: var(--tidal-blue); } +.ds-pixel[data-skin='cube'] .ds-pixel-c--m { color: #5b7a9e; } +.ds-pixel[data-skin='cube'] .ds-pixel-c--r { color: var(--accent); } +.ds-pixel[data-skin='cube'] .ds-pixel-row--feet { color: var(--tidal-blue); } + +/* Crest: the Pi mark worn on the head */ +.ds-pixel[data-skin='crest'] { --pixel-body: var(--accent); } +.ds-pixel[data-skin='crest'] .ds-pixel-row--pi { color: #ffffff; } + +/* Blueprint: accent linework on the page grid */ +.ds-pixel[data-skin='blueprint'] { --pixel-body: color-mix(in srgb, var(--accent) 88%, white); } +.ds-pixel[data-skin='blueprint'] .ds-pixel-row--feet { color: var(--tidal-blue); } + +/* Paper: pi.dev light theme, evening blue on moonstone */ +.ds-pixel[data-skin='paper'] { --pixel-body: #252f3d; } +.ds-pixel[data-skin='paper'][data-state='idle'] { --pixel-signal: #252f3d; } +.ds-pixel[data-skin='paper'][data-state='thinking'] { --pixel-signal: #3f8a58; } +.ds-pixel[data-skin='paper'][data-state='input'] { --pixel-signal: #b0712a; } +.ds-pixel[data-skin='paper'][data-state='blocked'] { --pixel-signal: var(--terracotta); } +.ds-pixel[data-skin='paper'][data-state='done'] { --pixel-signal: var(--tidal-blue); } +.ds-pixel[data-skin='paper'] .ds-pixel-row--pi { color: #09090b; } + +@keyframes ds-live-shake { + 0%, 22%, 100% { translate: 0 0; } + 4%, 12% { translate: -4px 0; } + 8%, 16% { translate: 4px 0; } +} + +@keyframes ds-live-call { + 0%, 60%, 100% { transform: translateY(0) scale(1); } + 14% { transform: translateY(-8px) scale(1.05, 1.1); } + 28% { transform: translateY(0) scale(1.1, 0.9); } + 40% { transform: translateY(-4px) scale(1); } +} + +@keyframes ds-live-spark { + 0%, 38% { opacity: 0; transform: translate(0, 0) scale(0.4); } + 48% { opacity: 1; transform: translate(calc(var(--sx) * 6px), calc(var(--sy) * 6px)) scale(1); } + 80%, 100% { opacity: 0; transform: translate(calc(var(--sx) * 16px), calc(var(--sy) * 14px)) scale(0.6); } +} + +@keyframes ds-live-orbit { + from { offset-distance: 0%; } + to { offset-distance: 100%; } +} + +@keyframes ds-fold-call { + 0%, 60%, 100% { transform: translateY(0) rotateZ(0); } + 15% { transform: translateY(-14px) rotateZ(-6deg); } + 30% { transform: translateY(0) rotateZ(4deg); } + 42% { transform: translateY(-5px) rotateZ(0); } +} + +@media (prefers-reduced-motion: reduce) { + .ds-live *, + .ds-live-body, + .ds-live-dot { + animation: none !important; + } + + .ds-live-code rect { transform: none; } +} + +/* ── pi.dev set ───────────────────────────────────────────────── + Mascots that are not cubes, plus stamps, rules, and icons. + A look sets four inks. A state sets --pm-signal, which lands on one part. */ + +.ds-pm { + --pm-ground: #0d1116; + --pm-body: var(--parchment); + --pm-line: var(--moonstone); + --pm-eye: #13110f; + --pm-signal: var(--pm-line); + color: var(--pm-line); + background: var(--pm-ground); +} + +.ds-pm[data-look='blueprint'] { + --pm-ground: var(--bg-canvas); + --pm-body: hsl(208 50% 60% / 0.14); + --pm-line: var(--accent); + --pm-eye: var(--accent); + background-image: + repeating-linear-gradient(90deg, hsl(218 60% 80% / 0.14) 0 1px, transparent 1px 20px), + repeating-linear-gradient(0deg, hsl(218 60% 80% / 0.14) 0 1px, transparent 1px 20px), + repeating-linear-gradient(90deg, hsl(218 60% 80% / 0.045) 0 1px, transparent 1px 4px), + repeating-linear-gradient(0deg, hsl(218 60% 80% / 0.045) 0 1px, transparent 1px 4px); +} + +.ds-pm[data-look='paper'] { + --pm-ground: #ebe7e4; + --pm-body: #252f3d; + --pm-line: #252f3d; + --pm-eye: #ebe7e4; +} + +.ds-pm[data-look='warm'] { + --pm-ground: #13110f; + --pm-body: var(--sunkissed); + --pm-line: var(--terracotta-light); + --pm-eye: #13110f; +} + +.ds-pm[data-state='thinking'] { --pm-signal: var(--success); } +.ds-pm[data-state='input'] { --pm-signal: var(--warning); } +.ds-pm[data-state='blocked'] { --pm-signal: var(--error); } +.ds-pm[data-state='done'] { --pm-signal: var(--accent); } +.ds-pm[data-look='paper'][data-state='thinking'] { --pm-signal: #3f8a58; } +.ds-pm[data-look='paper'][data-state='input'] { --pm-signal: #b0712a; } +.ds-pm[data-look='paper'][data-state='blocked'] { --pm-signal: var(--terracotta); } +.ds-pm[data-look='paper'][data-state='done'] { --pm-signal: var(--tidal-blue); } +.ds-pm[data-look='blueprint'][data-state='done'] { --pm-signal: var(--moonstone); } + +.ds-pm-svg { + display: block; + width: 100%; + max-width: 200px; + height: auto; + overflow: visible; +} + +.ds-pm-fill { + fill: var(--pm-body); + stroke: var(--pm-line); + stroke-width: 2.5; + stroke-linejoin: miter; + transition: fill 240ms ease, stroke 240ms ease; +} + +.ds-pm-shadow { fill: #000; opacity: 0.3; } +.ds-pm[data-look='paper'] .ds-pm-shadow { opacity: 0.12; } + +.ds-pm-signal { fill: var(--pm-signal); transition: fill 240ms ease; } + +.ds-pm-body, +.ds-pm-glyph-dot, +.ds-pm-corners, +.ds-pm-stamp-type, +.ds-pm-eyes, +.ds-pm-eye, +.ds-pm-trail rect { + transform-box: fill-box; + transform-origin: 50% 100%; +} + +.ds-pm-eyes, +.ds-pm-eye, +.ds-pm-corners, +.ds-pm-stamp-type { transform-origin: center; } + +.ds-pm-eye { + fill: var(--pm-eye); + animation: ds-pm-blink 4.8s infinite; +} + +.ds-pm-eyes { transition: transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1); } + +.ds-pm-mouth { + fill: none; + stroke: var(--pm-eye); + stroke-width: 3; + stroke-linecap: square; + opacity: 0; + transition: opacity 160ms ease; +} + +.ds-pm[data-state='idle'] .ds-pm-mouth--flat, +.ds-pm[data-state='thinking'] .ds-pm-mouth--flat, +.ds-pm[data-state='input'] .ds-pm-mouth--open, +.ds-pm[data-state='blocked'] .ds-pm-mouth--frown, +.ds-pm[data-state='done'] .ds-pm-mouth--smile { opacity: 1; } + +.ds-pm[data-state='thinking'] .ds-pm-eyes { transform: translate(4px, -3px); } +.ds-pm[data-state='input'] .ds-pm-eyes { transform: scale(1.25); } +.ds-pm[data-state='blocked'] .ds-pm-eyes { transform: translateY(4px) scaleY(0.3); } + +/* Pip */ + +.ds-pm-shine { + fill: none; + stroke: var(--pm-ground); + stroke-width: 4; + stroke-linecap: square; + opacity: 0.35; +} + +.ds-pm[data-look='paper'] .ds-pm-shine, +.ds-pm[data-look='blueprint'] .ds-pm-shine { stroke: var(--pm-line); } + +.ds-pm-pip { animation: ds-pm-rock 3.2s ease-in-out infinite; } +.ds-pm-pip-bead { animation: ds-pm-orbit 9s linear infinite; } +.ds-pm[data-state='thinking'] .ds-pm-pip-bead { animation-duration: 1.4s; } +.ds-pm[data-state='input'] .ds-pm-pip-bead { animation: ds-pm-orbit 0.7s cubic-bezier(0.6, 0, 0.4, 1) infinite alternate; } +.ds-pm[data-state='blocked'] .ds-pm-pip-bead { animation-play-state: paused; } +.ds-pm[data-state='input'] .ds-pm-pip { animation: ds-pm-call 1.3s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='blocked'] .ds-pm-pip { animation: ds-pm-shake 2.2s ease-in-out infinite; } +.ds-pm[data-state='done'] .ds-pm-pip { animation: ds-pm-roll 2s cubic-bezier(0.5, 0, 0.3, 1) infinite; transform-origin: 50% 58%; } + +/* Brackets */ + +.ds-pm-brackets-fill { stroke: none; opacity: 0.1; } + +/* The body is see-through, so the face takes the line ink. */ +.ds-pm-brackets .ds-pm-eye { fill: var(--pm-line); } +.ds-pm-brackets .ds-pm-mouth { stroke: var(--pm-line); } + +.ds-pm-corner { + fill: none; + stroke: var(--pm-line); + stroke-width: 6; + stroke-linecap: square; + stroke-linejoin: miter; + animation: ds-pm-breathe 2.6s ease-in-out infinite alternate; + transition: stroke 240ms ease; +} + +.ds-pm:not([data-state='idle']) .ds-pm-corner { stroke: var(--pm-signal); } +.ds-pm[data-state='thinking'] .ds-pm-corners { animation: ds-pm-quarter 2.4s cubic-bezier(0.7, 0, 0.2, 1) infinite; } +.ds-pm[data-state='input'] .ds-pm-corner { animation: ds-pm-hug 0.9s ease-in-out infinite alternate; } +.ds-pm[data-state='blocked'] .ds-pm-corners { rotate: -8deg; } +.ds-pm[data-state='blocked'] .ds-pm-corner { animation: none; stroke-dasharray: 6 5; } +.ds-pm[data-state='done'] .ds-pm-corner { animation: ds-pm-burst 1.8s cubic-bezier(0.3, 0, 0.2, 1) infinite; } + +/* Caret */ + +.ds-pm-caret-block { animation: ds-pm-caret 1.1s steps(1, end) infinite; } +.ds-pm-trail rect { opacity: 0; } +.ds-pm[data-state='thinking'] .ds-pm-caret-block { animation: none; } +.ds-pm[data-state='thinking'] .ds-pm-caret { animation: ds-pm-type 0.5s steps(1, end) infinite; } +.ds-pm[data-state='thinking'] .ds-pm-trail rect { + animation: ds-pm-trail 2s steps(1, end) infinite; + animation-delay: calc(var(--i) * 0.25s); +} +.ds-pm[data-state='input'] .ds-pm-caret { rotate: 8deg; transition: rotate 300ms cubic-bezier(0.3, 1.6, 0.4, 1); } +.ds-pm[data-state='input'] .ds-pm-caret-block { fill: var(--pm-signal); animation-duration: 0.5s; } +.ds-pm[data-state='blocked'] .ds-pm-caret-block { animation: none; fill: none; stroke: var(--pm-signal); stroke-dasharray: 8 6; } +.ds-pm[data-state='blocked'] .ds-pm-caret .ds-pm-eye { fill: var(--pm-signal); } +.ds-pm[data-state='done'] .ds-pm-caret-block { animation: none; fill: var(--pm-signal); } +.ds-pm[data-state='done'] .ds-pm-caret { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; } + +/* Glyph */ + +.ds-pm-glyph-dot { animation: ds-pm-bob 2.4s ease-in-out infinite; } +.ds-pm:not([data-state='idle']) .ds-pm-dot { fill: var(--pm-signal); } +.ds-pm[data-state='thinking'] .ds-pm-glyph-dot { animation: ds-pm-circuit 1.8s linear infinite; } +.ds-pm[data-state='input'] .ds-pm-glyph-dot { animation: ds-pm-jump 0.9s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='blocked'] .ds-pm-glyph-dot { animation: none; translate: 10px 20px; rotate: 24deg; } +.ds-pm[data-state='blocked'] .ds-pm-glyph { animation: ds-pm-shake 2.2s ease-in-out infinite; } +.ds-pm[data-state='done'] .ds-pm-glyph-dot { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='done'] .ds-pm-glyph { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) 0.15s infinite; --hop: 10px; } + +/* Stamp */ + +.ds-pm-ring { + fill: none; + stroke: var(--pm-eye); + stroke-width: 1.5; + stroke-dasharray: 2 4; + opacity: 0.6; +} + +.ds-pm-ring-text { + fill: var(--pm-eye); + font-family: var(--accent-mono); + font-size: 10px; + letter-spacing: 2.2px; +} + +.ds-pm:not([data-state='idle']) .ds-pm-ring-text { fill: var(--pm-signal); } +.ds-pm[data-look='ink']:not([data-state='idle']) .ds-pm-ring-text, +.ds-pm[data-look='warm']:not([data-state='idle']) .ds-pm-ring-text { fill: var(--pm-eye); } +.ds-pm[data-look='ink']:not([data-state='idle']) .ds-pm-stamp .ds-pm-fill, +.ds-pm[data-look='warm']:not([data-state='idle']) .ds-pm-stamp .ds-pm-fill { stroke: var(--pm-signal); stroke-width: 5; } +.ds-pm-stamp-type { animation: ds-pm-spin 24s linear infinite; } +.ds-pm[data-state='thinking'] .ds-pm-stamp-type { animation-duration: 4s; } +.ds-pm[data-state='input'] .ds-pm-stamp-type { animation-play-state: paused; } +.ds-pm[data-state='input'] .ds-pm-stamp { animation: ds-pm-wobble 1.2s ease-in-out infinite; transform-origin: center; } +.ds-pm[data-state='blocked'] .ds-pm-stamp-type { animation-play-state: paused; } +.ds-pm[data-state='blocked'] .ds-pm-stamp { rotate: -12deg; transform-origin: center; } +.ds-pm[data-state='done'] .ds-pm-stamp { animation: ds-pm-press 1.8s cubic-bezier(0.3, 0, 0.2, 1) infinite; transform-origin: center; } + +@keyframes ds-pm-blink { + 0%, 92%, 100% { transform: scaleY(1); } + 95% { transform: scaleY(0.1); } +} + +@keyframes ds-pm-rock { + 0%, 100% { rotate: -4deg; } + 50% { rotate: 4deg; } +} + +@keyframes ds-pm-orbit { + from { offset-distance: 0%; } + to { offset-distance: 100%; } +} + +@keyframes ds-pm-call { + 0%, 60%, 100% { transform: translateY(0) scale(1); } + 15% { transform: translateY(-12px) scale(0.96, 1.06); } + 30% { transform: translateY(0) scale(1.08, 0.92); } + 42% { transform: translateY(-4px); } +} + +@keyframes ds-pm-shake { + 0%, 22%, 100% { translate: 0 0; } + 5%, 14% { translate: -4px 0; } + 9%, 18% { translate: 4px 0; } +} + +@keyframes ds-pm-roll { + 0%, 12% { transform: translateY(0) rotate(0); } + 45% { transform: translateY(-18px) rotate(200deg); } + 70% { transform: translateY(0) rotate(360deg) scale(1.06, 0.94); } + 82%, 100% { transform: translateY(0) rotate(360deg); } +} + +@keyframes ds-pm-breathe { + from { translate: 0 0; } + to { translate: calc(var(--dx) * 3px) calc(var(--dy) * 3px); } +} + +@keyframes ds-pm-quarter { + 0% { rotate: 0deg; } + 25% { rotate: 90deg; } + 50% { rotate: 180deg; } + 75% { rotate: 270deg; } + 100% { rotate: 360deg; } +} + +@keyframes ds-pm-hug { + from { translate: 0 0; } + to { translate: calc(var(--dx) * -7px) calc(var(--dy) * -7px); } +} + +@keyframes ds-pm-burst { + 0%, 20% { translate: 0 0; } + 38% { translate: calc(var(--dx) * 12px) calc(var(--dy) * 12px); } + 60%, 100% { translate: 0 0; } +} + +@keyframes ds-pm-caret { + 0% { opacity: 1; } + 50% { opacity: 0.18; } +} + +@keyframes ds-pm-type { + 0% { translate: 0 0; } + 50% { translate: 2px 0; } +} + +@keyframes ds-pm-trail { + 0% { opacity: 0; } + 12%, 80% { opacity: 1; } + 90% { opacity: 0; } +} + +@keyframes ds-pm-hop { + 0%, 92%, 100% { transform: translateY(0) scale(1); } + 12% { transform: translateY(0) scale(1.08, 0.88); } + 34% { transform: translateY(calc(var(--hop, 26px) * -1)) scale(0.95, 1.06); } + 52% { transform: translateY(calc(var(--hop, 26px) * -1)); } + 68% { transform: translateY(0) scale(1.08, 0.9); } + 80% { transform: translateY(0) scale(0.98, 1.02); } +} + +@keyframes ds-pm-bob { + 0%, 100% { translate: 0 0; } + 50% { translate: 0 -5px; } +} + +@keyframes ds-pm-circuit { + 0% { translate: 0 0; } + 25% { translate: 0 -66px; } + 50% { translate: -92px -66px; } + 60% { translate: -92px -66px; opacity: 0.4; } + 75% { translate: -92px 0; opacity: 1; } + 100% { translate: 0 0; } +} + +@keyframes ds-pm-jump { + 0%, 100% { transform: translateY(0) scale(1.1, 0.9); } + 40% { transform: translateY(-34px) scale(0.94, 1.06); } + 55% { transform: translateY(-36px); } +} + +@keyframes ds-pm-spin { + to { rotate: 360deg; } +} + +@keyframes ds-pm-wobble { + 0%, 100% { rotate: -4deg; } + 50% { rotate: 4deg; } +} + +@keyframes ds-pm-press { + 0%, 30% { scale: 1; } + 40% { scale: 0.86; } + 52% { scale: 1.06; } + 64%, 100% { scale: 1; } +} + +/* Assets */ + +.ds-pa-stamp { + display: inline-block; + padding: 0.35rem 0.7rem 0.3rem; + border: 3px double currentColor; + font-family: var(--accent-mono); + font-size: 0.8rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--pm-line); + mix-blend-mode: normal; +} + +.ds-pa-stamp[data-tone='thinking'] { color: var(--success); } +.ds-pa-stamp[data-tone='input'] { color: var(--warning); } +.ds-pa-stamp[data-tone='blocked'] { color: var(--error); } +.ds-pa-stamp[data-tone='done'] { color: var(--accent); } +.ds-pm[data-look='paper'] .ds-pa-stamp[data-tone='thinking'] { color: #3f8a58; } +.ds-pm[data-look='paper'] .ds-pa-stamp[data-tone='input'] { color: #b0712a; } +.ds-pm[data-look='paper'] .ds-pa-stamp[data-tone='blocked'] { color: var(--terracotta); } +.ds-pm[data-look='paper'] .ds-pa-stamp[data-tone='done'] { color: var(--tidal-blue); } + +.ds-pa-rule { + width: 100%; + height: 12px; + color: var(--pm-line); +} + +.ds-pa-rule--ruler { + background: + repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 20px) bottom / 100% 12px no-repeat, + repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 4px) bottom / 100% 5px no-repeat, + linear-gradient(currentColor, currentColor) bottom / 100% 1px no-repeat; + opacity: 0.7; +} + +.ds-pa-rule--dotted { + background: radial-gradient(circle, currentColor 1.2px, transparent 1.6px) center / 8px 8px repeat-x; + opacity: 0.6; +} + +.ds-pa-rule--thread { + background: linear-gradient(90deg, transparent, var(--tidal-blue) 12%, var(--pm-line) 50%, var(--tidal-blue) 88%, transparent) center / 100% 2px no-repeat; +} + +.ds-pa-rule--mark { + display: flex; + align-items: center; + gap: 0.8rem; + height: auto; +} + +.ds-pa-rule--mark span { + flex: 1; + height: 1px; + background: currentColor; + opacity: 0.45; +} + +.ds-pa-icon { + display: block; + fill: none; + stroke: currentColor; + stroke-width: 1.75; + stroke-linecap: square; + stroke-linejoin: miter; +} + +.ds-pa-icon--pi { fill: currentColor; stroke: none; } + +@media (prefers-reduced-motion: reduce) { + .ds-pm *, + .ds-pm *::before { + animation: none !important; + transition: none !important; + } +} + +/* ── Glyph family ─────────────────────────────────────────────── + The Pi mark stood up. Inks and state come from the surrounding .ds-pm. */ + +.ds-gl-mark { display: block; overflow: visible; } +.ds-gl-mark .ds-pm-eye { animation-delay: 0.4s; } + +.ds-gl-svg .ds-pm-eyes, +.ds-gl-rider, +.ds-gl-walker, +.ds-gl-follower, +.ds-gl-cell, +.ds-gl-cells, +.ds-gl-peek-eyes { + transform-box: fill-box; + transform-origin: 50% 100%; +} + +.ds-gl-peek-eyes, +.ds-gl-cell { transform-origin: center; } + +/* Peek */ + +.ds-gl-peek-room { fill: var(--pm-line); opacity: 0.08; } +.ds-gl-peek-eye { fill: var(--pm-line); } +.ds-gl-peek-eyes { animation: ds-gl-look 6s ease-in-out infinite; } +.ds-pm[data-state='thinking'] .ds-gl-peek-eyes { animation: ds-gl-scan 1.2s steps(4, end) infinite; } +.ds-pm[data-state='input'] .ds-gl-peek-eyes { animation: none; transform: scale(1.35); } +.ds-pm[data-state='input'] .ds-gl-peek-body { animation: ds-pm-call 1.3s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='blocked'] .ds-gl-peek-eyes { animation: none; transform: translateY(6px) scaleY(0.4); } +.ds-pm[data-state='done'] .ds-gl-peek-eyes { animation: none; transform: scaleY(0.35); } +.ds-pm[data-state='done'] .ds-gl-peek-body { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) 0.12s infinite; --hop: 14px; } + +/* Carry */ + +.ds-gl-rider { animation: ds-gl-sway 3s ease-in-out infinite; } +.ds-pm[data-state='thinking'] .ds-gl-rider { animation: ds-gl-juggle 0.9s cubic-bezier(0.3, 0, 0.3, 1) infinite; transform-origin: center; } +.ds-pm[data-state='input'] .ds-gl-rider { animation: ds-gl-wave 0.5s ease-in-out infinite alternate; } +.ds-pm[data-state='blocked'] .ds-gl-rider { animation: none; transform-origin: center; transform: translate(59px, 99px) rotate(90deg); } +.ds-pm[data-state='blocked'] .ds-gl-carry-body { animation: ds-pm-shake 2.2s ease-in-out infinite; } +.ds-pm[data-state='blocked'] .ds-gl-carry-body > .ds-pm-eyes { transform: translate(6px, 4px) scaleY(0.4); } +.ds-pm[data-state='done'] .ds-gl-carry-body { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; --hop: 18px; } +.ds-pm[data-state='done'] .ds-gl-rider { animation: ds-gl-lift 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='thinking'] .ds-gl-carry-body > .ds-pm-eyes { transform: translateY(-3px); } + +/* Walk */ + +.ds-gl-walk { overflow: hidden; } +.ds-gl-road rect { fill: var(--pm-line); opacity: 0.5; } +.ds-gl-walker { animation: ds-pm-bob 3s ease-in-out infinite; } +.ds-gl-follower { animation: ds-pm-bob 3s ease-in-out 0.3s infinite; } +.ds-pm[data-state='thinking'] .ds-gl-road { animation: ds-gl-road 0.42s linear infinite; } +.ds-pm[data-state='thinking'] .ds-gl-walker { animation: ds-gl-step 0.84s ease-in-out infinite; } +.ds-pm[data-state='thinking'] .ds-gl-follower { animation: ds-gl-skip 0.42s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='thinking'] .ds-gl-walker .ds-pm-eyes { transform: translateX(3px); } +.ds-pm[data-state='input'] .ds-gl-follower { animation: ds-pm-jump 0.9s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='input'] .ds-gl-walker .ds-pm-eyes { transform: translateX(-4px) scale(1.2); } +.ds-pm[data-state='blocked'] .ds-gl-walker { animation: ds-gl-strain 1.6s ease-in-out infinite; } +.ds-pm[data-state='blocked'] .ds-gl-walker .ds-pm-eyes { transform: translateX(3px) scaleY(0.35); } +.ds-pm[data-state='blocked'] .ds-gl-follower { animation: none; translate: 10px 0; } +.ds-pm[data-state='done'] .ds-gl-walker { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; --hop: 20px; } +.ds-pm[data-state='done'] .ds-gl-follower { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) 0.18s infinite; --hop: 28px; } + +/* Assemble */ + +.ds-gl-slot { + fill: none; + stroke: var(--pm-signal); + stroke-width: 2; + stroke-dasharray: 4 4; + opacity: 0; +} + +.ds-gl-cells { transform-origin: 50% 100%; } +.ds-gl-assemble-eyes { transition: opacity 200ms ease; } +.ds-pm[data-state='thinking'] .ds-gl-cell { + animation: ds-gl-scatter 3s cubic-bezier(0.6, 0, 0.3, 1) infinite; + animation-delay: calc(var(--i) * 0.04s); +} +.ds-pm[data-state='thinking'] .ds-gl-assemble-eyes { animation: ds-gl-eyes-away 3s ease infinite; } +.ds-pm[data-state='input'] .ds-gl-slot { opacity: 0.9; } +.ds-pm[data-state='input'] .ds-gl-cell--dot { animation: ds-gl-hover 1s ease-in-out infinite alternate; } +.ds-pm[data-state='blocked'] .ds-gl-cell { + transform: translate(var(--px), var(--py)) rotate(var(--pr)); + transition: transform 700ms cubic-bezier(0.5, 0, 0.8, 0.4); + transition-delay: calc(var(--i) * 0.03s); +} +.ds-pm[data-state='blocked'] .ds-gl-assemble-eyes { opacity: 0; } +.ds-pm[data-state='done'] .ds-gl-cells { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; --hop: 16px; } +.ds-pm[data-state='done'] .ds-gl-assemble-eyes { animation: ds-pm-hop 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; --hop: 16px; } +.ds-pm[data-state='done'] .ds-gl-cell { animation: ds-gl-flash 1.7s ease-out infinite; } + +/* Extrude */ + +.ds-gl-extrude { + position: relative; + display: grid; + place-items: center; + width: 100%; + min-height: 300px; + perspective: 800px; +} + +.ds-gl-extrude-spin { + position: relative; + width: 150px; + height: 141px; + transform-style: preserve-3d; + animation: ds-gl-turn 4.5s ease-in-out infinite alternate; +} + +.ds-gl-layer { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + overflow: visible; + transform: translateZ(calc(var(--z) * -3px)); +} + +.ds-gl-side { fill: color-mix(in srgb, var(--pm-body) 42%, #05070a); } +.ds-gl-layer--back .ds-gl-side { stroke: var(--pm-line); stroke-width: 1.5; } +.ds-pm[data-look='paper'] .ds-gl-side { fill: color-mix(in srgb, var(--pm-body) 70%, #ebe7e4); } +.ds-pm[data-look='blueprint'] .ds-gl-side { fill: hsl(208 50% 60% / 0.1); stroke: hsl(208 50% 60% / 0.35); stroke-width: 1; } + +.ds-gl-extrude-floor { + position: absolute; + bottom: 30px; + width: 190px; + height: 18px; + border-radius: 50%; + background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent); +} + +.ds-pm[data-look='paper'] .ds-gl-extrude-floor { background: radial-gradient(closest-side, rgb(37 47 61 / 0.18), transparent); } +.ds-pm[data-state='thinking'] .ds-gl-extrude-spin { animation: ds-gl-spin 3s linear infinite; } +.ds-pm[data-state='input'] .ds-gl-extrude-spin { animation: ds-gl-nudge 0.8s ease-in-out infinite alternate; } +.ds-pm[data-state='blocked'] .ds-gl-extrude-spin { animation: none; transform: rotateX(24deg) rotateY(-52deg) rotateZ(-8deg); } +.ds-pm[data-state='done'] .ds-gl-extrude-spin { animation: ds-gl-flip 2s cubic-bezier(0.6, 0, 0.2, 1) infinite; } + +/* Wordmark */ + +.ds-gl-word { + display: flex; + align-items: flex-end; + gap: 18px; +} + +.ds-gl-word-text { + font-family: var(--serif); + font-size: 92px; + font-weight: 400; + line-height: 0.8; + letter-spacing: -0.02em; + color: var(--pm-line); +} + +.ds-gl-i { + position: relative; + display: inline-block; +} + +.ds-gl-tittle { + position: absolute; + left: 50%; + top: -0.05em; + width: 0.14em; + height: 0.14em; + margin-left: -0.07em; + background: var(--pm-signal); + animation: ds-pm-bob 2.8s ease-in-out infinite; +} + +.ds-pm[data-state='thinking'] .ds-gl-tittle { animation: ds-gl-tittle-bounce 0.6s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='input'] .ds-gl-tittle { animation: ds-gl-tittle-wait 1.4s cubic-bezier(0.3, 0, 0.3, 1) infinite; } +.ds-pm[data-state='blocked'] .ds-gl-tittle { animation: none; translate: 0.26em 0.62em; rotate: 90deg; } +.ds-pm[data-state='done'] .ds-gl-tittle { animation: ds-gl-tittle-flip 1.7s cubic-bezier(0.3, 0, 0.3, 1) infinite; } + +/* Pixel */ + +.ds-gl-pixel { + margin: 0; + font-family: Menlo, "SF Mono", "DejaVu Sans Mono", monospace; + font-size: 22px; + line-height: 1; + white-space: pre; + color: var(--pm-line); +} + +.ds-gl-pixel-dot { color: var(--pm-signal); } + +@keyframes ds-gl-look { + 0%, 20%, 100% { transform: translateX(0); } + 30%, 45% { transform: translateX(-5px); } + 55%, 70% { transform: translateX(5px); } + 80% { transform: translate(0, -3px); } +} + +@keyframes ds-gl-scan { + 0% { transform: translate(-5px, -3px); } + 25% { transform: translate(0, -4px); } + 50% { transform: translate(5px, -3px); } + 75% { transform: translate(0, 2px); } +} + +@keyframes ds-gl-sway { + 0%, 100% { rotate: -3deg; } + 50% { rotate: 3deg; } +} + +@keyframes ds-gl-juggle { + 0%, 100% { transform: translateY(0) rotate(0); } + 45% { transform: translateY(-34px) rotate(90deg); } + 55% { transform: translateY(-34px) rotate(90deg); } + 90% { transform: translateY(0) rotate(180deg); } +} + +@keyframes ds-gl-wave { + from { rotate: -14deg; } + to { rotate: 14deg; } +} + +@keyframes ds-gl-lift { + 0%, 50%, 100% { translate: 0 0; } + 62% { translate: 0 -16px; } + 76% { translate: 0 0; } +} + +@keyframes ds-gl-road { + from { transform: translateX(0); } + to { transform: translateX(-12px); } +} + +@keyframes ds-gl-step { + 0%, 100% { transform: rotate(-4deg) translateY(0); } + 25% { transform: rotate(0) translateY(-4px); } + 50% { transform: rotate(4deg) translateY(0); } + 75% { transform: rotate(0) translateY(-4px); } +} + +@keyframes ds-gl-skip { + 0%, 100% { transform: translateY(0) scale(1.06, 0.94); } + 50% { transform: translateY(-10px) scale(0.96, 1.04); } +} + +@keyframes ds-gl-strain { + 0%, 100% { transform: rotate(10deg) translateX(0); } + 50% { transform: rotate(13deg) translateX(3px); } +} + +@keyframes ds-gl-scatter { + 0%, 12%, 84%, 100% { transform: translate(0, 0) rotate(0); } + 34%, 60% { transform: translate(var(--sx), var(--sy)) rotate(var(--sr)); } +} + +@keyframes ds-gl-eyes-away { + 0%, 12%, 88%, 100% { opacity: 1; } + 24%, 76% { opacity: 0; } +} + +@keyframes ds-gl-hover { + from { transform: translateY(-10px); } + to { transform: translateY(-20px); } +} + +@keyframes ds-gl-flash { + 0%, 60%, 100% { fill: var(--pm-body); } + 68% { fill: var(--pm-signal); } +} + +@keyframes ds-gl-turn { + from { transform: rotateX(-10deg) rotateY(-38deg); } + to { transform: rotateX(-10deg) rotateY(38deg); } +} + +@keyframes ds-gl-spin { + from { transform: rotateX(-10deg) rotateY(0deg); } + to { transform: rotateX(-10deg) rotateY(360deg); } +} + +@keyframes ds-gl-nudge { + from { transform: rotateX(-6deg) rotateY(-14deg) translateY(0); } + to { transform: rotateX(-14deg) rotateY(14deg) translateY(-10px); } +} + +@keyframes ds-gl-flip { + 0%, 15% { transform: rotateX(-10deg) rotateY(0) translateY(0); } + 45% { transform: rotateX(-10deg) rotateY(180deg) translateY(-26px); } + 75%, 100% { transform: rotateX(-10deg) rotateY(360deg) translateY(0); } +} + +@keyframes ds-gl-tittle-bounce { + 0%, 100% { translate: 0 0; scale: 1.15 0.85; } + 50% { translate: 0 -0.18em; scale: 0.92 1.08; } +} + +@keyframes ds-gl-tittle-wait { + 0%, 100% { translate: 0 0; } + 20%, 70% { translate: 0 -0.34em; } +} + +@keyframes ds-gl-tittle-flip { + 0%, 20% { translate: 0 0; rotate: 0deg; } + 50% { translate: 0 -0.4em; rotate: 180deg; } + 75%, 100% { translate: 0 0; rotate: 360deg; } +} + +@media (prefers-reduced-motion: reduce) { + .ds-gl-extrude-spin, + .ds-gl-tittle, + .ds-gl-cell { animation: none !important; transition: none !important; } +} + +/* ── Stamps ───────────────────────────────────────────────────── + One ink per tone. Paper darkens each ink so it holds on moonstone. */ + +.ds-st-defs { position: absolute; width: 0; height: 0; overflow: hidden; } + +.ds-st { --st-ink: var(--pm-line, var(--moonstone)); --st-ground: var(--pm-ground, #0d1116); color: var(--st-ink); } +.ds-st[data-tone='thinking'] { --st-ink: var(--success); } +.ds-st[data-tone='input'] { --st-ink: var(--warning); } +.ds-st[data-tone='blocked'] { --st-ink: var(--error); } +.ds-st[data-tone='done'] { --st-ink: var(--accent); } +.ds-pm[data-look='paper'] .ds-st[data-tone='thinking'] { --st-ink: #3f8a58; } +.ds-pm[data-look='paper'] .ds-st[data-tone='input'] { --st-ink: #b0712a; } +.ds-pm[data-look='paper'] .ds-st[data-tone='blocked'] { --st-ink: var(--terracotta); } +.ds-pm[data-look='paper'] .ds-st[data-tone='done'] { --st-ink: var(--tidal-blue); } +.ds-pm[data-look='warm'] .ds-st[data-tone='done'] { --st-ink: var(--sunkissed); } + +.ds-st[data-inked='true'] { filter: url(#ds-ink); } + +.ds-st-line { fill: none; stroke: var(--st-ink); } +.ds-st-ink-fill { fill: var(--st-ink); } + +.ds-st-type { + fill: var(--st-ink); + font-family: var(--accent-mono); + letter-spacing: 0.14em; +} + +.ds-st-ring { font-size: 12.5px; letter-spacing: 0.2em; } +.ds-st-band { fill: var(--st-ground); font-size: 13px; letter-spacing: 0.16em; } +.ds-st-small { font-size: 10px; letter-spacing: 0.18em; } +.ds-st-date { font-size: 10px; letter-spacing: 0; } + +/* Label */ + +.ds-st-label { + display: inline-block; + padding: 0.4rem 0.8rem 0.35rem; + border: 4px double var(--st-ink); + font-family: var(--accent-mono); + font-size: 0.95rem; + letter-spacing: 0.16em; + text-transform: uppercase; + white-space: nowrap; +} + +/* Seal */ + +.ds-st-seal { display: block; width: 180px; height: 180px; overflow: visible; } + +/* Ticket */ + +.ds-st-ticket { + --notch: 11px; + display: grid; + grid-template-columns: auto 1fr; + width: 340px; + max-width: 100%; + color: var(--st-ground); + background: var(--st-ink); + mask: + radial-gradient(circle var(--notch) at 0 50%, transparent 98%, #000) left / 51% 100% no-repeat, + radial-gradient(circle var(--notch) at 100% 50%, transparent 98%, #000) right / 51% 100% no-repeat; +} + +.ds-st-ticket-stub { + display: grid; + justify-items: center; + align-content: center; + gap: 8px; + padding: 16px 18px 14px 22px; + border-right: 2px dashed var(--st-ground); + font-family: var(--accent-mono); + font-size: 0.72rem; + letter-spacing: 0.1em; +} + +.ds-st-ticket-stub svg { width: 52px; height: auto; } +.ds-st-ticket-stub .ds-st-ink-fill { fill: var(--st-ground); } + +.ds-st-ticket-body { + display: grid; + gap: 4px; + align-content: center; + padding: 14px 24px 14px 18px; + min-width: 0; +} + +.ds-st-ticket-kicker, +.ds-st-ticket-word { + font-family: var(--accent-mono); + font-size: 0.66rem; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.ds-st-ticket-title { + overflow: hidden; + font-family: var(--serif); + font-size: 1.35rem; + font-style: italic; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ds-st-ticket-word { + justify-self: start; + margin-top: 4px; + padding: 2px 6px; + border: 1.5px solid var(--st-ground); +} + +/* Postmark */ + +.ds-st-postmark { display: block; width: 300px; max-width: 100%; height: auto; overflow: visible; } + +/* Review */ + +.ds-st-review { + width: 260px; + border: 3px solid var(--st-ink); + font-family: var(--accent-mono); + font-size: 0.7rem; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.ds-st-review-head { + padding: 6px 10px; + color: var(--st-ground); + background: var(--st-ink); + letter-spacing: 0.2em; +} + +.ds-st-review dl { + display: grid; + grid-template-columns: auto 1fr; + gap: 8px 14px; + margin: 0; + padding: 12px 12px 14px; +} + +.ds-st-review dt { opacity: 0.7; } +.ds-st-review dd { margin: 0; border-bottom: 1px dotted currentColor; padding-bottom: 2px; text-transform: none; font-family: var(--mono); letter-spacing: 0; } + +.ds-st-review-result { + position: relative; + justify-self: start; + padding: 0 6px; + border-bottom: 0 !important; + font-family: var(--accent-mono) !important; + letter-spacing: 0.14em !important; + text-transform: uppercase !important; +} + +.ds-st-review-result svg { + position: absolute; + inset: -9px -12px; + width: calc(100% + 24px); + height: calc(100% + 18px); + overflow: visible; +} + +/* Stickers */ + +.ds-st-sticker { + display: block; + width: 120px; + height: auto; + overflow: visible; + filter: drop-shadow(0 6px 8px rgb(0 0 0 / 0.35)); + transition: rotate 300ms cubic-bezier(0.3, 1.6, 0.4, 1), translate 300ms cubic-bezier(0.3, 1.6, 0.4, 1); +} + +.ds-st-sticker:hover { rotate: 0deg !important; translate: 0 -4px; } +.ds-st-sticker[aria-label='Built with pi sticker'], +.ds-st-sticker[aria-label='Ship it sticker'] { width: 190px; } +.ds-st-sticker-edge { fill: #ebe7e4; } + +.ds-st-sticker-ring { + fill: #13110f; + font-family: var(--accent-mono); + font-size: 9.5px; + letter-spacing: 0.12em; +} + +.ds-st-sticker-pi { + fill: #13110f; + font-family: var(--serif); + font-size: 46px; + font-style: italic; +} + +.ds-st-sticker-word { + fill: #ebe7e4; + font-family: var(--serif); + font-size: 25px; + font-style: italic; +} + +.ds-st-sticker-word--ink { + fill: #13110f; + font-family: var(--accent-mono); + font-size: 17px; + font-style: normal; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +/* Press */ + +.ds-st-press { + position: relative; + display: grid; + place-items: center; + animation: ds-st-press 520ms cubic-bezier(0.5, 0, 0.2, 1) both; +} + +.ds-st-splash { + position: absolute; + inset: 12%; + border: 2px solid var(--pm-signal, var(--accent)); + border-radius: 50%; + opacity: 0; + pointer-events: none; + animation: ds-st-splash 700ms ease-out 180ms both; +} + +@keyframes ds-st-press { + 0% { opacity: 0; transform: scale(1.7) rotate(8deg); filter: blur(2px); } + 55% { opacity: 1; transform: scale(0.94) rotate(-1deg); filter: blur(0); } + 75% { transform: scale(1.02) rotate(0); } + 100% { opacity: 1; transform: scale(1) rotate(0); } +} + +@keyframes ds-st-splash { + 0% { opacity: 0.8; transform: scale(0.7); } + 100% { opacity: 0; transform: scale(1.5); } +} + +@media (prefers-reduced-motion: reduce) { + .ds-st-press, + .ds-st-splash { animation: none; } + .ds-st-splash { display: none; } +} + +/* ── Schematic ────────────────────────────────────────────────── + A drawing sheet. Linework in the look's line ink, the live state in the signal ink. */ + +@property --bp-spread { + syntax: ''; + inherits: true; + initial-value: 0px; +} + +.ds-bp-sheet { + display: block; + width: 100%; + height: auto; + overflow: visible; +} + +.ds-bp-frame { fill: none; stroke: var(--pm-line); stroke-width: 1.5; } +.ds-bp-frame--inner { stroke-width: 2.5; } +.ds-bp-ext { fill: none; stroke: var(--pm-line); stroke-width: 1; opacity: 0.6; } +.ds-bp-construct { fill: none; stroke: var(--pm-line); stroke-width: 1; stroke-dasharray: 2 4; opacity: 0.35; } +.ds-bp-center { fill: none; stroke: var(--pm-line); stroke-width: 1; stroke-dasharray: 14 4 3 4; opacity: 0.55; } +.ds-bp-arrow { fill: var(--pm-line); } +.ds-bp-arrow--hot { fill: var(--pm-signal); } + +.ds-bp-dimline { + fill: none; + stroke: var(--pm-line); + stroke-width: 1.25; + stroke-dasharray: 1; + animation: ds-bp-draw 1.1s cubic-bezier(0.6, 0, 0.2, 1) 0.2s both; +} + +.ds-bp-label { + fill: var(--pm-line); + font-family: var(--accent-mono); + font-size: 12px; + letter-spacing: 0.1em; +} + +.ds-bp-small { font-size: 10.5px; } +.ds-bp-tiny { font-size: 9px; opacity: 0.65; } +.ds-bp-note { font-size: 9.5px; letter-spacing: 0.04em; opacity: 0.85; } +.ds-bp-zone { font-size: 10px; opacity: 0.7; } +.ds-bp-view-title { font-size: 12.5px; letter-spacing: 0.16em; } + +.ds-bp-title { + fill: var(--pm-line); + font-family: var(--serif); + font-size: 26px; + font-style: italic; +} + +.ds-bp-part { stroke-width: 2; } +.ds-pm[data-look='blueprint'] .ds-bp-part { fill: hsl(208 50% 60% / 0.16); } +.ds-pm[data-look='blueprint'] .ds-bp-eye { fill: var(--pm-line); } + +.ds-bp-depth { fill: color-mix(in srgb, var(--pm-line) 10%, transparent); stroke: var(--pm-line); stroke-width: 1; } + +.ds-bp-pin { fill: var(--pm-line); } +.ds-bp-leader { fill: none; stroke: var(--pm-line); stroke-width: 1; } +.ds-bp-balloon { fill: var(--pm-ground); stroke: var(--pm-line); stroke-width: 1.25; } +.ds-bp-balloon-n { font-size: 11px; letter-spacing: 0; } + +.ds-bp-callout { + animation: ds-bp-fade 500ms ease both; + animation-delay: calc(0.5s + var(--n) * 0.18s); +} + +.ds-bp-callout:nth-child(n) .ds-bp-pin { fill: var(--pm-line); } +.ds-bp-view > .ds-bp-callout:last-of-type .ds-bp-pin { fill: var(--pm-signal); } + +/* Exploded view breathes: the cells part and close along their own grid offsets */ +.ds-bp-explode { animation: ds-bp-explode 6s cubic-bezier(0.6, 0, 0.3, 1) infinite; } +.ds-bp-cell { transform: translate(calc(var(--cx) * var(--bp-spread)), calc(var(--cy) * var(--bp-spread))); } + +.ds-bp-hot-row .ds-bp-label, +.ds-bp-hot-text .ds-bp-label:last-child { fill: var(--pm-signal); } + +/* State schema */ + +.ds-bp-edge-line { fill: none; stroke: var(--pm-line); stroke-width: 1.25; opacity: 0.7; } + +.ds-bp-edge--hot .ds-bp-edge-line { + stroke: var(--pm-signal); + stroke-width: 2; + stroke-dasharray: 6 5; + opacity: 1; + animation: ds-bp-flow 0.8s linear infinite; +} + +.ds-bp-node rect { fill: var(--pm-ground); stroke: var(--pm-line); stroke-width: 1.5; } + +.ds-bp-node--hot rect { + fill: color-mix(in srgb, var(--pm-signal) 16%, var(--pm-ground)); + stroke: var(--pm-signal); + stroke-width: 2.5; + animation: ds-bp-pulse 1.6s ease-in-out infinite; +} + +.ds-bp-node--hot .ds-bp-label { fill: var(--pm-signal); } + +.ds-bp-seal { opacity: 0.9; } +.ds-bp-seal .ds-st-seal { animation: ds-st-press 520ms cubic-bezier(0.5, 0, 0.2, 1) 0.9s both; transform-box: fill-box; transform-origin: center; } + +@keyframes ds-bp-draw { + from { stroke-dashoffset: 1; } + to { stroke-dashoffset: 0; } +} + +@keyframes ds-bp-fade { + from { opacity: 0; translate: -6px 0; } + to { opacity: 1; translate: 0 0; } +} + +@keyframes ds-bp-explode { + 0%, 18%, 100% { --bp-spread: 0px; } + 42%, 76% { --bp-spread: 16px; } +} + +@keyframes ds-bp-flow { + to { stroke-dashoffset: -11; } +} + +@keyframes ds-bp-pulse { + 0%, 100% { stroke-opacity: 1; } + 50% { stroke-opacity: 0.45; } +}