{"id":"9b7c68c7-c078-4767-91df-c9eb11f51b1c","slug":"peg-design-system","kind":"skill","name":"peg-design-system","summary":"Minimal, type-led HTML/CSS/React design system for slide decks and data-driven presentations. 极简演示文稿设计系统，含7种幻灯片模板与React组件。","owner":"v0id-byte","repoUrl":"https://github.com/v0id-byte/peg-design-system","license":"MIT","licenseTier":"permissive","trust":"community","stars":8,"match":null,"fewStars":false,"format":"skill","createdAt":"2026-06-11T07:42:06.000Z","addedAt":"2026-10-14T06:28:13.630Z","logoUrl":"https://avatars.githubusercontent.com/u/222865629?u=d7b7ff40fc6ebe274fa6b891203ec9a20ac566f2&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["design-media","data-analytics","browser-web"],"tags":["css-tokens","data-visualization","design-system","design-tokens","html-slides","minimal-design","open-source","presentation"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-17T08:32:49.475Z","sample":false,"url":"https://conduits.dev/tools/peg-design-system","language":"HTML","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Peg Design System — Minimal Slide & Presentation Framework\n\n> A type-led, data-first design system for slide decks, web presentations, and React UI components. Built on restraint: one background, one accent, no shadows.\n\n**[English](#english) · [中文](#中文)**\n\n---\n\n## Preview\n\n| Title slide | Types comparison |\n|---|---|\n| ![Title slide](docs/screenshots/01-title.png) | ![Types comparison](docs/screenshots/02-types.png) |\n\n| Data comparison table | Mechanism slide |\n|---|---|\n| ![Data comparison table](docs/screenshots/03-comparison.png) | ![Mechanism slide](docs/screenshots/04-mechanism.png) |\n\n![Depeg chart — pure SVG + CSS, no chart library](docs/screenshots/05-depeg-chart.png)\n\n---\n\n## English\n\n### What is Peg?\n\n**Peg** is an open-source design system and slide framework extracted from a production presentation deck. It gives you a complete, opinionated visual language for building beautiful, readable presentations in HTML/CSS and React — without touching a design tool. Ships with a Claude Skill (`SKILL.md`) for direct import into **Claude Design**.\n\nKey characteristics:\n- **Minimal palette** — 6 tokens, one warm-white background, one jade accent\n- **Type-led hierarchy** — Inter variable font, aggressively negative tracking, no decorative elements\n- **Data-first** — tabular numerals, SVG 2px chart lines, count-up animations built in\n- **Flat & fast** — no shadows, no gradients, no card chrome; content lives directly on paper\n- **Accessible by default** — all animations respect `prefers-reduced-motion`; DOM always shows final state\n\n---\n\n### Use Cases\n\n| Scenario | What to use |\n|---|---|\n| Build a slide deck in HTML | `slides/` templates + `slides/slide-base.css` |\n| Prototype a data-heavy UI | `components/core/` React components |\n| Adopt the design tokens only | `tokens/*.css` |\n| Browse the design language | `guidelines/*.card.html` |\n\n---\n\n### Eight Slide Templates\n\n| Template | Purpose |\n|---|---|\n| **Title** | Deck opener — wordmark + $1.00 peg line |\n| **Headline** | One bold claim + supporting sentence |\n| **Stats** | Two key figures with short descriptors |\n| **Flow** | Left-to-right value or process chain |\n| **Chips** | Labelled category grid with context |\n| **Attr List** | Define something with 3–4 bold-term properties |\n| **Moment** | Centred full-frame question or provocation |\n| **Media / Proof** | Show real renders, CAD, PCB, photos, screenshots — full-bleed, split (text + figure), or a 2–4 figure proof wall |\n\n#### Media / Proof slides\n\nThree image layouts, all on-language (hairline frames, no shadows, captions in caption type):\n\n- `<section class=\"media-full\">` — the image **is** the slide (Night letterbox). Children: `<img class=\"media-img contain\">` (use `contain` to show a whole part, drop it to fill/crop), optional `.media-scrim`, `.media-label`, `.media-title`, `.media-cap`.\n- `<section class=\"paper media-split\">` — text on the left (`.media-text`), one framed `<figure class=\"media-figure\">` on the right.\n- `<section class=\"paper media-wall\">` — a `.wall` of 2–4 `<figure class=\"cell\">`, each with a `.fig` and a `<figcaption>`.\n\nEvery image slide should carry an evidence tag (`.evi` → RENDER / CAD / PCB / PROTOTYPE / APP) and a one-line caption, so the audience knows what they're looking at and that it's real.\n\n`media-wall`'s row height and headline size are both pinned by CSS for real photography, not just curated screenshots: the wall's single grid row is forced to `1fr` (real product photos with mixed aspect ratios could otherwise inflate the row past the wall's own box and clip captions), and `.media-wall .headline` is stepped down to 56px so a two-line title never runs into the wall below it. Don't override these back to `auto` / the default 92px unless every image and headline on that slide has been checked at full length.\n\n---\n\n### Five Core React Components\n\n| Component | Purpose |\n|---|---|\n| `PegLine` | The signature $1.00 horizontal baseline |\n| `FlowBand` | Arrow-chained flow diagram |\n| `Chip` | Bordered pill for key/value metadata |\n| `StatBlock` | Large formatted number + descriptor |\n| `AttrList` | Stacked property definition rows |\n\n---\n\n### Design Tokens\n\n```\ntokens/\n  colors.css      — 6 palette values + semantic aliases\n  typography.css  — 7 type roles, tracking, weight\n  spacing.css     — spacing scale + 1920×1080 canvas geometry\n  effects.css     — line weights, radii, motion curves\n  fonts.css       — Inter variable @font-face\n```\n\n---\n\n### Colour Palette\n\n| Token | Value | Role |\n|---|---|---|\n| `--color-paper` | `#FAFAF8` | Only background — warm, never cold |\n| `--color-ink` | `#16181D` | Primary text and lines |\n| `--color-night` | `#0B0B0C` | Stage chrome, outer letterbox |\n| `--color-jade` | `#00A878` | Default accent — one use per slide |\n| `--color-alert` | `#E5484D` | Danger only — losses, risk, collapse |\n| `--color-mute` | `#8A8A8E` | Labels, footnotes, secondary text |\n\n**Topic accents (muted, meaning-bearing).** For a multi-section deck, give each section its own *calm* hue instead of jade everywhere — set `--accent: var(--topic-…)` on the `<section>` and every accent utility (`.jade` text, `.evi` tag, diagram strokes `.s-jade`/`.box-j`/`.t-j`/`.f-jade`) follows it automatically. Keep them muted (Apple-keynote, never neon) and consistent per module (eyebrow = keyword = diagram = same hue).\n\n| Token | Value | Meaning |\n|---|---|---|\n| `--topic-control` | `#128A6B` | teal — real-time control / signal |\n| `--topic-silicon` | `#3A6AA0` | blue — electronics / PCB |\n| `--topic-material` | `#8C6B38` | bronze — material / industrial design |\n| `--topic-safety` | `#AC5A3C` | clay — safety-critical (not alarm-red) |\n| `--topic-algo` | `#6A57A0` | violet — algorithm / intelligence |\n| `--topic-growth` | `#4F8A52` | sage — data / growth |\n\n**Layout patterns.** `.headline.with-fig` steps the headline down a size when it shares the slide with a diagram; `.media-split .media-text .headline` does the same for a narrow text column; `.summary` is a centered one-line takeaway placed *below* a diagram so the eye reads the diagram then drops to the conclusion; **`.plain`** is a one-sentence *plain-language explainer* (accent left-rule) placed under the headline for a non-expert audience — the slide keeps its technical content, this line just makes it land for everyone (bold the one phrase that matters; on a headline+figure slide cap its `max-width` to the text column; mind 2-line headlines when setting `top`). See template 11 in `slides/`. **`.compare`** is a before→after / problem→solution table (rows of *old limitation* → *what we now do*) — the clearest way to show **iteration** at a glance; `.badge-first` flags genuinely first-of-kind rows (use sparingly, back the claim). See template 12.\n\n---\n\n### Typography Scale\n\n| Role | Size | Tracking | Weight |\n|---|---|---|---|\n| Wordmark | 248px | −0.04em | 700 |\n| Hero | 300px | −0.04em | 700 |\n| Headline | 92px | −0.03em | 600 |\n| Subhead | 60px | −0.02em | 600 |\n| Support | 44px | −0.01em | 400 |\n| Caption | 28px | 0 | 400 |\n\n---\n\n### Quick Start\n\n```html\n<!-- Link the token chain -->\n<link rel=\"stylesheet\" href=\"styles.css\">\n<link rel=\"stylesheet\" href=\"slides/slide-base.css\">\n\n<!-- Use a slide template -->\n<div class=\"slide slide--headline\">\n  <div class=\"frame\">\n    <p class=\"label\">SECTION LABEL</p>\n    <h1 class=\"headline\">A bold one-line claim.</h1>\n    <p class=\"support\">Supporting sentence that adds context.</p>\n  </div>\n</div>\n```\n\n---\n\n### File Structure\n\n```\nstyles.css              Entry point (@import chain)\ntokens/                 CSS custom properties\ncomponents/core/        React components (.jsx + .d.ts + .prompt.md)\nslides/                 7 HTML slide templates + base CSS\nguidelines/             Visual documentation cards\nassets/                 Inter variable font\n```\n\n---\n\n### Design Principles\n\n1. **One background.** Paper (`#FAFAF8`) only. No dark slides, no gradients.\n2. **One accent.** Jade once per slide, maximum. Never decorative.\n3. **Alert is earned.** `#E5484D` for genuine bad news only.\n4. **No shadows.** No elevation. Content lands on paper.\n5. **Final state first.** Screenshots and PDFs always show complete slides.\n6. **Tabular numerals everywhere.** No column-width jitter.\n7. **Negative tracking on display text.** Tight, not loose.\n\n---\n\n### Motion & Animation\n\nPeg includes a built-in animation engine (`peg-animate.js`) for Keynote-like entrance effects and slide transitions.\n\n#### How it is put together\n\nThe engine is not a set of CSS animations with Keynote-ish names. Keynote is closer to a small motion-graphics renderer, and reproducing it needed two things CSS keyframes alone do not give you: **segmented time** (builds overshoot and settle, each segment on its own curve) and **material layers** (masks, specular sweeps — not just geometry).\n\n```\nApple bundle (*.parameterGroup / deck protobuf)\n        ↓   tools/keynote-params.py · tools/keynote-effects.py\n   effect fact database\n        ↓\n   effect descriptor\n        ↓\n     Motion IR            ← peg-animate.d.ts defines the contract\n        ↓\n ┌──────┴──────┐\n Geometry   Material      ← Geometry owns `transform`. Material owns\n compositor compositor       everything else and may NEVER write transform.\n └──────┬──────┘\n      WAAPI\n```\n\nTwo properties are worth knowing because they are what keep this maintainable:\n\n- **The compositors never see an effect name.** They handle `Target → Track → property`. Adding an effect means adding a descriptor that emits tracks — not touching a renderer. If you find yourself writing `if (effect === '…')` inside a compositor, the abstraction has already failed.\n- **Compositor ownership is machine-checked**, not documented and hoped for. `motion-regression/fixtures/*/ownership.json` records which compositor wrote which property, and its `violations` list must stay empty. A material track reaching `transform` silently corrupts the geometry compositor's own composition, so it is caught by the harness rather than by review.\n\nEach compositor emits its **own** animation. Keynote gives every channel a separate curve — Fade and Move travels on `cubic-bezier(.311,.208,.2013,1)` while its opacity runs `cubic-bezier(.7028,0,.3007,.9992)` — and a WAAPI keyframe carries one easing for the whole segment. Merging them would make the transform quietly adopt the opacity's curve, which is exactly the flattening this design removes.\n\n#### Where the numbers come from\n\nNot from taste, and not from Keynote's HTML export. Two extractors read the source of truth directly:\n\n| Tool | Reads | Gives |\n|---|---|---|\n| `tools/keynote-params.py` | Apple's `*.parameterGroup` archives inside Keynote.app | every effect's parameters and its per-segment Bézier curves, with a version lock |\n| `tools/keynote-effects.py` | a `.key` file's Snappy/protobuf payload | which effects a deck uses, and each one's exact duration |\n\nBoth label evidence. Parameters, curves, effect counts and durations are `exact`; the mapping from a parameter name to an animation phase is `inferred` and says so, because the file does not record it.\n\n> **Do not use Keynote's HTML export as a reference.** Its own `kpfAnimationFallbacks.plist` downgrades `FromDarkness`, `LineDraw`, `LineDrawForLine` and `wipe` to a plain `apple:dissolve`. Timings measured from that export describe a dissolve, not the effect your deck actually uses — which is how those effects went missing from this library in the first place.\n\n#### Verifying a change\n\n```bash\nnode motion-regression/capture-frame.js\n```\n\nSee [motion-regression/README.md](motion-regression/README.md). Failures are classified by which artefact disagrees — IR, compositor, ownership contract, or just browser rendering.\n\n#### Motion Tokens (in `tokens/effects.css`)\n\n| Token | Value | Purpose |\n|---|---|---|\n| `--ease-reveal` | `cubic-bezier(0.2, 0.65, 0.2, 1)` | Standard entrance easing |\n| `--ease-out-expo` | `cubic-bezier(0.16, 1, 0.3, 1)` | Fast deceleration, snappy entries |\n| `--ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | Overshoot spring, for hero numbers |\n| `--ease-in-out` | `cubic-bezier(0.65, 0, 0.35, 1)` | Symmetric, for slide transitions |\n| `--dur-fast` | `200ms` | Quick micro-interactions |\n| `--dur-normal` | `400ms` | Standard UI transitions |\n| `--dur-slow` | `820ms` | Default build animation duration |\n| `--dur-draw` | `1400ms` | SVG path drawing (e.g., depeg chart) |\n| `--dur-page` | `500ms` | Slide transition duration |\n\n**Keynote-parity tokens.** These were measured from Keynote's own HTML export of the source decks, not guessed — see `tools/keynote-effects.py`.\n\n| Token | Value | Purpose |\n|---|---|---|\n| `--ease-keynote` | `cubic-bezier(0.42, 0, 0.58, 1)` | CAMediaTimingFunction easeInEaseOut — Keynote's default on 5977 of 7242 measured animations. Symmetric, unlike `--ease-reveal` |\n| `--dur-build` | `500ms` | Dominant Keynote build duration |\n| `--dur-zoom-big` | `350ms` | `apple:bc-zoom-big`, measured |\n| `--dur-transition` | `1000ms` | Keynote slide transition |\n| `--spread-delivery` | `333ms` | Per-unit delivery window (see Delivery below) |\n| `--move-distance` | `28px` | `fade-move` offset |\n| `--zoom-big-from` | `10` | `zoom-big` starts this many × size |\n\n#### Build Animations (Entrance Effects)\n\nAdd `data-peg-animate` to any element inside a slide:\n\n| Type | Keyframes | Best For |\n|---|---|---|\n| `fade-up` | `opacity 0→1, translateY(22px)→0` | Default for text, containers |\n| `scale-in` | `opacity 0→1, scale(0.8)→1` | Hero numbers, wordmarks |\n| `blur-in` | `opacity 0→1, filter blur(8px)→0` | Background elements |\n| `reveal-right` | `opacity 0→1, translateX(30px)→0` | Flow nodes, list items |\n| `letter-spring` | each character `opacity 0→1, translateY(0.72em)→0` with spring overshoot | One-line title or moment slides |\n| `text-flip` | `opacity 0→1, translateY(0.42em)→0, rotateX(-82deg)→0` | Calm single-line text reveal |\n| `draw` | SVG stroke draws on along its own path (`stroke-dashoffset` len→0) | **Lines, waveforms, chart curves, flow connectors** — a transient left-to-right *sweep*, never a perpetual loop |\n\n**Keynote-parity builds.** Reproductions of the effects actually used in the source decks — run `tools/keynote-effects.py` on a `.key` to see which ones a deck uses.\n\n| Type | Keynote effect | Keyframes | Best for |\n|---|---|---|---|\n| `fade-move` | `apple:fade and move` | `opacity 0→1` + offset `--move-distance` along `data-peg-direction` | Titles and body lines. The single most-used effect in the source decks (117×) |\n| `zoom-big` | `apple:bc-zoom-big` | `opacity 0→1, scale(10)→1` | Numbers, one key word. Loud — once per slide |\n| `pop` | `apple:bc-pop` | `opacity 0→1, scale(0.6)→1` with spring | Small cards, chips, legends; pairs with `data-peg-stagger` |\n| `wipe` | `apple:wipe` | directional `clip-path: inset()` | Bars, rules, colour blocks. Restrained — nothing moves |\n| `cube` | `apple:bc-3D-cube` | `rotateX(-90°)→0` with 1200px perspective, backface hidden | Section titles, anything wanting a \"turn over\" reading |\n| `from-darkness` | `com.apple.iWork.Keynote.FromDarkness` — **Fade and Scale / 渐变和缩放** | scale + late opacity + a gradient reveal + a 70° specular sweep | Hero lines and cards **on a dark ground**. The second most-used effect in the source decks (89×) |\n| `line-draw` | `com.apple.iWork.Keynote.LineDrawForLine` — **Line Draw / 画线** | SVG stroke draws on, start cap pops to 1.3×, pen tip rides the path | Charts, flow connectors, signatures, hand-drawn marks (32×) |\n| `keyboard` | `apple:keyboard` — **Keyboard / 键盘** | text types out chunk by chunk behind a blinking caret | One line you want the room to read *with* you (5×) |\n\n**These effects are why the library exists.** Keynote's own HTML export throws them away: `kpfAnimationFallbacks.plist` inside the app bundle downgrades `FromDarkness`, `LineDraw`, `LineDrawForLine` and `wipe` to a plain `apple:dissolve`. Reproducing them from Apple's `*.parameterGroup` definitions gets closer to the original deck than Apple's own exporter does.\n\n**Builds are not single eases.** Every Keynote build overshoots and settles, each segment on its own curve, and that is what makes the motion feel like Keynote rather than like CSS:\n\n| Effect | Measured path |\n|---|---|\n| `zoom-big` | `10 → 1.08 @62% → 0.95 @82% → 1` |\n| `pop` | `0.6 → 1.125 @43% → 0.952 @66.7% → 1` |\n| `cube` | `−90° → −6.02° @52% → +2.86° @60% → 0°` |\n| `clothesline` | five damped bounces: `0.31 / −0.21 / 0.07 / −0.014 / 0.0017` |\n\nRun `tools/keynote-params.py <Effect>` to see any effect's own numbers and curves.\n\n#### Fade and Scale — dark ground required\n\n`from-darkness` reveals through a gradient mask, so it only reads as \"emerging from darkness\" against a dark surface. On a light slide it degrades to a scale-and-fade. Where `mask-image` is unsupported the reveal is skipped and the build still plays as scale + opacity + shine rather than failing to appear.\n\n#### Line Draw — SVG only\n\nPut it on a `<path>`/`<line>` with a stroke. The pen tip and start cap are created inside the path's own `<svg>` and **removed when the build settles**, so the resting DOM is a plain finished stroke for screenshots, PDF and PPTX export. Path geometry is sampled once and cached; the cache invalidates on resize or `viewBox` change so the tip cannot drift off its line.\n\n`draw` (the original, simpler sweep) is unchanged and still available.\n\n#### Keyboard — deterministic by design\n\nChunk lengths vary, but from a seeded generator, so a replay types at exactly the same rhythm — Keynote behaves the same way, and a truly random rhythm could never be regression-tested. Override with `data-peg-seed`. `data-peg-cursor=\"off\"` drops the caret and clamps chunks to 0.05–2.5s absolute, matching Keynote's cursorless mode. The caret is **3% of the line height** wide and blinks on a 0.4s half-cycle.\n\n`data-peg-direction` (`top` / `bottom` / `left` / `right`) steers `fade-move` and `wipe`; `data-peg-distance` overrides the offset.\n\n#### Delivery — per character or per word\n\n`data-peg-delivery=\"character\"` (or `\"word\"`) animates each unit separately.\n\n```html\n<h1 data-peg-animate=\"fade-move\" data-peg-delivery=\"character\">逐字淡入并移动。</h1>\n```\n\nSplitting uses `Intl.Segmenter` where available, so whole grapheme clusters survive — family emoji (`👨‍👩‍👧‍👦`), skin-tone modifiers (`👍🏽`), flags, combining marks. Word mode is segmented too, so a Chinese sentence is not treated as one word.\n\n**Timing is a spread, not a fixed per-unit delay.** A line always lands within its `duration` no matter how many characters it has; each unit runs for the remainder after `--spread-delivery`. A fixed delay would drag a long sentence out over seconds. Set `data-peg-spread` to change the window, or `data-peg-letter-stagger` to force a fixed per-unit delay instead.\n\n> Keynote's own per-character rhythm is **not** recoverable — its HTML export rasterizes text builds into image sequences. The spread model is Peg's design decision, not a measured Keynote value.\n\n**Rich text is not split.** An element containing child markup (`<strong>`, `<a>`, …) keeps its structure, animates as one object, and logs a warning. Split spans get `aria-hidden`, and the parent gets an `aria-label` so screen readers read the line once.\n\n#### Build steps — click to advance\n\nPut `data-peg-step=\"N\"` on anything that should wait for a click. `→` walks the steps on the current slide first and only turns the page once they are exhausted; `←` rewinds one step.\n\n```html\n<h1 data-peg-animate=\"fade-move\">Now</h1>\n<div class=\"card\" data-peg-animate=\"fade-move\" data-peg-step=\"1\">Then</div>\n<div class=\"card\" data-peg-animate=\"fade-move\" data-peg-step=\"2\">Finally</div>\n```\n\n- Missing or invalid `data-peg-step` is treated as **0** (plays on slide entry). Invalid values warn.\n- Numbers may **skip** — `1, 3, 7` compresses to three consecutive logical steps.\n- Elements sharing a step fire **together**; per-character delivery inside each still staggers independently.\n- Rewinding restores that step's before-state instantly; earlier steps stay built. Backward animation reads as a glitch, so Peg does not play one.\n- Un-revealed content gets `visibility: hidden` and `pointer-events: none`, so it is never clickable, focusable, or read by assistive tech before its turn.\n- Jumping by number key or thumbnail lands **fully built**, as does stepping back to an earlier slide.\n\n#### Actions — animating something already on screen\n\n| Action | Keynote effect | Attributes |\n|---|---|---|\n| `data-peg-action=\"move\"` | `apple:action-motion-path` | `data-peg-x` / `data-peg-y` (px), optional `data-peg-path` |\n| `data-peg-action=\"scale\"` | `apple:action-scale` | `data-peg-scale` |\n| `data-peg-action=\"opacity\"` | `apple:action-opacity` | `data-peg-opacity` |\n\nAn element carries at most one action, so chain several by pointing separate directive elements at the same target with `data-peg-target`. The directive never renders, and the engine accumulates the changes — step 2 keeps step 1's move:\n\n```html\n<div class=\"dot\" id=\"d\"></div>\n<i data-peg-action=\"move\"    data-peg-target=\"#d\" data-peg-x=\"700\" data-peg-step=\"1\"></i>\n<i data-peg-action=\"scale\"   data-peg-target=\"#d\" data-peg-scale=\"1.8\" data-peg-step=\"2\"></i>\n<i data-peg-action=\"opacity\" data-peg-target=\"#d\" data-peg-opacity=\"0.25\" data-peg-step=\"3\"></i>\n```\n\n`data-peg-path` (an SVG path `d`) is **progressive enhancement**: it uses CSS `offset-path` in the element's own coordinate system, with `offset-rotate: 0deg` so the object does not rotate along the curve. Where `offset-path` is unsupported it falls back to the straight `data-peg-x`/`-y` move, so the end state is identical either way.\n\n**Author transforms are preserved.** A `transform` you already set — `rotate(-3deg)`, `translateX(-50%)` — is captured before the engine touches the element and composed as a prefix, so animation never swallows it. It is captured once, at compile time; a transform that changes responsively afterwards will not be picked up.\n\n**Drawing lines & flows (the silky bit).** Put `data-peg-animate=\"draw\"` on a **solid** SVG `<path>`/`<line>` (not an intentionally-dashed one) and it sweeps on left-to-right; duration follows `--dur-draw`, override with `data-peg-duration`. For a **flow / timeline that reveals left-to-right**, draw the baseline/connector with `draw` and stagger the nodes one-by-one (`data-peg-stagger` on the parent `<g>`, `data-peg-animate=\"fade-up\"` on each node) with increasing `data-peg-delay`. The resting DOM is the full solid stroke, so screenshots / PDF export always show the finished line.\n\n**Delays:** Use `data-peg-delay=\"80\"` (ms) for individual timing.<br>\n**Stagger:** Add `data-peg-stagger=\"100\"` to a parent for automatic sibling delays.<br>\n**Letter stagger:** Use `data-peg-letter-stagger=\"34\"` (ms) for per-character timing.\n\n```html\n<div class=\"headline\" data-peg-animate=\"fade-up\" data-peg-delay=\"80\">Title</div>\n<div class=\"hero\" data-peg-animate=\"scale-in\" data-peg-delay=\"200\">$306B</div>\n<div class=\"headline\" data-peg-animate=\"letter-spring\">A one-line moment.</div>\n<div class=\"chips\" data-peg-stagger=\"100\">\n  <div class=\"chip\" data-peg-animate=\"fade-up\">Item 1</div>\n  <div class=\"chip\" data-peg-animate=\"fade-up\">Item 2</div>\n</div>\n```\n\nOpen `letter-spring-demo.html` to preview the per-character title reveal.\n\n**Backward compatible:** `.build.d1` through `.build.d5` classes still work (mapped to `fade-up` with 80/200/320/440/560ms delays).\n\n#### Count-Up Numbers\n\nAdd `data-peg-count` or use `.count` class with data attributes:\n\n```html\n<div class=\"stat-n count\" data-to=\"306\" data-pre=\"$\" data-suf=\"B\" data-cdelay=\"360\">$306B</div>\n```\n\n#### Data Bars\n\nAdd `data-peg-bar` to bars or use `.bar-fill` / `.progress-fill`. Horizontal bars grow left-to-right; vertical bars grow bottom-to-top.\n\n```html\n<div class=\"bar-fill\" style=\"width:72%;\"></div>\n<div data-peg-bar=\"vertical\" style=\"height:180px;\"></div>\n```\n\nUse `data-peg-bar-duration`, `data-peg-bar-delay`, and `data-peg-bar-easing` for local timing overrides. The resting DOM remains the final value for screenshots, PDF, and PPTX export.\n\n#### Slide Transitions\n\nAdd `data-peg-transition` to `<section>` (slide) elements:\n\n| Value | Effect |\n|---|---|\n| `dissolve` | Crossfade (old fades out, new fades in) — `apple:dissolve` |\n| `clothesline` | Pages swing and dip as they are pushed sideways, settling with a spring — `apple:ClotheslinePush`. Direction follows forward/back |\n| `push-left` | New slides in from right, old slides out left |\n| `text-flip` | Old slide flips upward while the new slide flips in from below on the same timeline |\n| `none` | Instant cut (default) |\n\n> Keynote's `apple:bc-3D-cube` is a **build**, not a transition — the `bc-` prefix means build. It is `data-peg-animate=\"cube\"` above, applied per object.\n\n#### Magic Move — `data-peg-transition=\"magic-move\"`\n\nThe most-used transition in the source decks: `$apple:magic-move-implied-motion-path`, 70 instances, on **28 of PianoTuner's 48 slides**. Objects the two slides share travel from where they were to where they are going; everything else cross-fades.\n\nPair objects with the same `data-peg-id` on both slides:\n\n```html\n<section class=\"paper\">\n  <div class=\"card\" data-peg-id=\"hero\">PCB v1</div>\n  <div class=\"version\" data-peg-id=\"ver\">V1.0</div>\n</section>\n\n<section class=\"paper\" data-peg-transition=\"magic-move\">\n  <div class=\"card wide\" data-peg-id=\"hero\">主控 + 电机</div>\n  <div class=\"version\" data-peg-id=\"ver\">Now</div>   <!-- morphs in place -->\n</section>\n```\n\n| Attribute | Default | Purpose |\n|---|---|---|\n| `data-peg-magic-duration` | `900` | Morph duration (ms) |\n| `data-peg-magic-easing` | `--ease-keynote` | Timing function |\n| `data-peg-magic-stagger` | auto | Per-item delay for *unmatched* content only |\n| `data-peg-magic-match=\"text\"` | off | Also pair plain-text elements with identical text |\n\n**The rule that makes it read as Apple rather than busy: things that travel, travel together; things that merely appear, stagger.** Matched objects all move simultaneously so the eye reads one coherent spatial event — staggering them reads as chaos. Only unmatched content, which has no prior position, arrives in sequence (auto-derived, capped at 150ms per item and 600ms total — the 150ms figure is Apple's own, measured from the `StaggeredFadeIn` component on apple.com).\n\nText that changes size morphs by **`font-size`**, not by transform scale, so glyphs stay crisp instead of blurring or thickening. Position uses centre-based FLIP, so any `transform` you already set still composes.\n\nMatching is by explicit `data-peg-id` because a silent mismatch is worse than no morph. `data-peg-magic-match=\"text\"` opts into text pairing per slide (the same tiered approach reveal.js auto-animate takes).\n\n#### The finale — modules converging on a lockup\n\nMagic Move is what makes the Apple-style summary slide work, where the modules introduced through the deck reassemble around a central lockup. Ground truth from the source deck: that slide has **no builds at all** — the entire collage arrives through the transition, with the shared centre element anchored and everything else fading in around it.\n\nSo it needs no new engine feature: give the centre element a `data-peg-id` shared with the previous slide, lay the modules around it, and set `data-peg-transition=\"magic-move\"`. See the last slide of `motion-library-demo.html`.\n\nLayout guidance, from analysis of Apple's published summary slides: cap at **~12 tiles**, exactly one hero; encode hierarchy by **tile area, not font size**; make the number the loudest thing on the slide (labels should be ignorable); keep one accent colour, everything else monochrome.\n\n```html\n<section class=\"paper\" data-label=\"Title\" data-peg-transition=\"text-flip\">...</section>\n```\n\nTransitions respect `prefers-reduced-motion` and are disabled when `noscale` is set on `<deck-stage>`. Open `transition-demo.html` to compare cut, dissolve, push-left, and text-flip.\n\n#### Auditing a Keynote deck\n\n`tools/keynote-effects.py` reads a `.key` directly (it is a zip of Snappy-compressed protobuf) and reports which animation effects it uses — useful when porting a deck to Peg and you need to know what to reproduce. Pure standard library, no dependencies.\n\n```bash\npython3 tools/keynote-effects.py deck.key --per-slide\n```\n\nOutput labels its own **evidence level**, because the claims are not equally well founded: effect counts and slide association are `exact` (the identifier string is literally present in a given slide's payload), while durations are `heuristic` — doubles found near the identifier, which proximity alone does not prove are that effect's duration. `--strict` drops the heuristic section; `--json` emits machine-readable output for tooling; `--debug-iwa` reports per-block decode diagnostics.\n\nFor exact timing, export the deck from Keynote as HTML instead and read the per-slide JSON — that is how the Keynote-parity tokens above were measured. Note that the HTML export rasterizes text builds, so per-character timing is not recoverable from it.\n\n`tools/fixtures/effect-counts.json` pins the counts for four reference decks, so a change to the Snappy/IWA decoding cannot regress silently.\n\n#### Initialization\n\n```html\n<script src=\"deck-stage.js\"></script>\n<script src=\"peg-animate.js\"></script>\n<script>\n  PegAnimate.init('deck-stage');\n</script>\n```\n\n---\n\n### Designing for Chinese (中文)\n\nPeg is bilingual. Latin (Inter) is tuned for tight negative tracking and compact\nleading; Chinese needs the opposite — square em-box glyphs collide under negative\ntracking and cramp under tight leading. `tokens/cjk.css` re-tunes the type tokens\n**only** when the document language is Chinese, so Latin decks are unaffected.\n\n- Set `lang=\"zh-CN\"` (or any `zh-…`) on `<html>` — the CJK layer activates\n  automatically: tracking relaxes toward zero, display leading opens up\n  (`--lh-tight` 1.12 / `--lh-snug` 1.20 / `--lh-body` 1.7).\n- Chinese glyphs render in **Noto Sans SC**; Latin/numerals stay in **Inter**\n  (per-glyph fallback), so a mixed line like “重写 ESP-IDF” looks right.\n- Use the `.latin` class on a pure-Latin element inside a Chinese deck to restore\n  Inter's tight tracking just for that element (e.g. a wordmark).\n- For an offline / portable / PDF-exported Chinese deck, bundle the font with the\n  deck (or a `pyftsubset` subset of the used glyphs) — don't rely on system fonts.\n\n### Fonts & Licenses\n\nThe design system's code is **MIT**. The bundled fonts are **not** MIT — they ship\nunder the **SIL Open Font License 1.1 (OFL)** and are redistributed here with their\nlicense text, as the OFL requires:\n\n| Font | Role | License | Source |\n|---|---|---|---|\n| **Inter** by Rasmus Andersson | Latin / numerals | SIL OFL 1.1 — [`assets/Inter-OFL.txt`](assets/Inter-OFL.txt) | github.com/rsms/inter |\n| **Noto Sans SC** by Google | Chinese (简体) | SIL OFL 1.1 — [`assets/NotoSansSC-OFL.txt`](assets/NotoSansSC-OFL.txt) | fonts.google.com/noto |\n\nUnder the OFL you may use, embed, and redistribute both fonts (incl. commercially)\nprovided the license text travels with them and the fonts are not sold on their own.\nKeep the `*-OFL.txt` files alongside `assets/*.ttf` in any redistribution.\n\n> Repo size note: `assets/NotoSansSC.ttf` is a full variable font (~17 MB). For a\n> shipped deck, subset it to the glyphs you actually use to stay lean.\n\n### License\n\n**Code & design tokens:** MIT — use freely in commercial and personal projects.\n**Bundled fonts (`assets/*.ttf`):** SIL OFL 1.1 (see *Fonts & Licenses* above).\n\n---\n\n---\n\n## 中文\n\n### Peg 是什么？\n\n**Peg** 是一套开源设计系统与幻灯片框架，从一套生产级演示文稿中提炼而来。它为你提供完整、有主张的视觉语言，让你用 HTML/CSS 和 React 构建美观、可读性强的演示文稿——无需打开任何设计工具。包含 Claude Skill（`SKILL.md`），可直接导入 **Claude Design** 使用。\n\n核心特点：\n- **极简配色** — 6 个色彩 token，一种暖白背景，一种翠绿强调色\n- **以字排版为核心** — Inter 可变字体，强负字间距，零装饰元素\n- **数据优先** — 等宽数字、SVG 2px 图表线、内置数字滚动动画\n- **扁平快速** — 无阴影、无渐变、无卡片边框；内容直接落在纸面上\n- **默认可访问** — 所有动画尊重 `prefers-reduced-motion`；DOM 始终展示最终状态\n\n---\n\n### 适用场景\n\n| 场景 | 使用内容 |\n|---|---|\n| 用 HTML 制作幻灯片 | `slides/` 模板 + `slides/slide-base.css` |\n| 原型设计数据密集型 UI | `components/core/` React 组件 |\n| 仅引入设计 token | `tokens/*.css` |\n| 浏览设计语言规范 | `guidelines/*.card.html` |\n\n---\n\n### 八种幻灯片模板\n\n| 模板 | 用途 |\n|---|---|\n| **Title（标题页）** | 封面——品牌字标 + $1.00 基准线 |\n| **Headline（主张页）** | 一句粗体核心论点 + 补充说明 |\n| **Stats（数据页）** | 两组关键数字 + 简短描述 |\n| **Flow（流程页）** | 从左到右的价值/流程链 |\n| **Chips（分类页）** | 带标签的分类网格 |\n| **Attr List（属性页）** | 3–4 条粗体术语定义列表 |\n| **Moment（转场页）** | 居中全屏问句或论点 |\n| **Media / Proof（实证页）** | 放真实渲染图/CAD/PCB/实物照/截图——整幅、图文左右分栏、或 2–4 图实证墙 |\n\n#### 实证 / 图片页\n\n三种图片版式,均遵循设计语言(细线边框、无阴影、说明用 caption 字号):\n\n- `<section class=\"media-full\">` — 整幅:图片**就是**整页(Night 信箱底)。子元素:`<img class=\"media-img contain\">`(`contain` 显示完整零件,去掉则填满裁切)、可选 `.media-scrim`、`.media-label`、`.media-title`、`.media-cap`。\n- `<section class=\"paper media-split\">` — 左文右图:左侧 `.media-text`,右侧一个带框 `<figure class=\"media-figure\">`。\n- `<section class=\"paper media-wall\">` — 实证墙:一个 `.wall` 里放 2–4 个 `<figure class=\"cell\">`,每个含 `.fig` + `<figcaption>`。\n\n每张图片页都应带**证据标签**(`.evi` → RENDER / CAD / PCB / PROTOTYPE / APP)和一句说明,让观众清楚自己看到的是什么、且确为真实产物。\n\n`media-wall` 的行高与标题字号都是为**真实产品照片**(而不只是精挑细选的截图)特意钉死的:墙内单行网格强制 `1fr`(否则不同长宽比的真实照片会把行高撑爆,超出墙自身的框、切掉下方说明文字);`.media-wall .headline` 也降到了 56px,防止两行标题压到下方的墙。除非已经用完整长度的图片和标题走过一遍效果,否则不要把它们改回 `auto` / 默认的 92px。\n\n---\n\n### 五个核心 React 组件\n\n| 组件 | 用途 |\n|---|---|\n| `PegLine` | 标志性 $1.00 水平基准线 |\n| `FlowBand` | 带箭头的流程图 |\n| `Chip` | 带边框的键值标签胶囊 |\n| `StatBlock` | 大字号数字 + 描述文字 |\n| `AttrList` | 堆叠式属性定义行 |\n\n---\n\n### 设计 Token\n\n```\ntokens/\n  colors.css      — 6 个调色板值 + 语义别名\n  typography.css  — 7 种文字角色、字间距、字重\n  spacing.css     — 间距比例 + 1920×1080 画布几何\n  effects.css     — 线宽、圆角半径、动效曲线\n  fonts.css       — Inter 可变字体 @font-face\n```\n\n---\n\n### 配色方案\n\n| Token | 色值 | 用途 |\n|---|---|---|\n| `--color-paper` | `#FAFAF8` | 唯一背景色，暖白 |\n| `--color-ink` | `#16181D` | 主文本与线条 |\n| `--color-night` | `#0B0B0C` | 舞台边框、外侧信箱区 |\n| `--color-jade` | `#00A878` | 单一强调色，每张幻灯片最多使用一次 |\n| `--color-alert` | `#E5484D` | 仅用于危险/损失/风险场景 |\n| `--color-mute` | `#8A8A8E` | 标签、注脚、次要文本 |\n\n---\n\n### 设计原则\n\n1. **只有一种背景**：Paper (`#FAFAF8`)，无深色页、无渐变页。\n2. **只有一种强调色**：Jade 每页最多出现一次，不用于装饰。\n3. **Alert 色需要理由**：`#E5484D` 仅用于真实的坏消息。\n4. **无阴影**：无高程层级，内容直接落在纸面。\n5. **最终状态优先**：截图与 PDF 导出始终显示完整幻灯片。\n6. **等宽数字无处不在**：防止列宽抖动。\n7. **标题文字强负字间距**：紧凑而非松散。\n\n---\n\n### 快速开始\n\n```html\n<!-- 引入 token 链 -->\n<link rel=\"stylesheet\" href=\"styles.css\">\n<link rel=\"stylesheet\" href=\"slides/slide-base.css\">\n\n<!-- 使用幻灯片模板 -->\n<div class=\"slide slide--headline\">\n  <div class=\"frame\">\n    <p class=\"label\">章节标签</p>\n    <h1 class=\"headline\">一句醒目的核心论点。</h1>\n    <p class=\"support\">补充一句上下文说明。</p>\n  </div>\n</div>\n```\n\n---\n\n### 动效补充\n\n大标题页的一行字可用 `data-peg-animate=\"letter-spring\"`，每个字母/汉字会从下方逐个弹性甩入并渐现，适合 Keynote 风格的 moment slide。用 `data-peg-letter-stagger=\"34\"` 调整逐字间隔。\n\n```html\n<div class=\"headline\" data-peg-animate=\"letter-spring\">A one-line moment.</div>\n```\n\n如果需要更克制、无弹性的单行文字翻入，可用 `data-peg-animate=\"text-flip\"`。\n\n**Keynote 对齐的一套动效。** 参数不是拍脑袋定的——是从本机 Keynote 15.3 应用包里 Apple 自己的 `*.parameterGroup` 定义解出来的，见 `tools/keynote-params.py`。\n\n| 类型 | Keynote 中文菜单名 | 适合用在哪 |\n|---|---|---|\n| `fade-move` | 渐变和移动 | 标题、要点句。四个 deck 里共 117 次，你最常用的一个 |\n| `from-darkness` | **渐变和缩放** | 主标题、卡片，**必须深色底**。第二常用（89 次）：渐变揭开 + 70° 光泽扫过 |\n| `zoom-big` | 放大 | 数字、单个关键词。冲击力强，一页最多一次 |\n| `pop` | 弹入 | 小卡片、标签、图例。配 `data-peg-stagger` 依次弹入 |\n| `wipe` | 划变 | 条状图形、分隔线、色块。沿一个方向擦出，不动位置 |\n| `cube` | 立体翻转 | 章节标题、需要「翻面」隐喻的单个对象 |\n| `line-draw` | **画线** | 图表、流程连线、手写标注（32 次）。起点弹跳 + 笔尖跟随 |\n| `keyboard` | **键盘** | 想让全场跟着你一起读的那一行（5 次）。打字机 + 闪烁光标 |\n\n**这三个新效果正是这套库存在的理由：** Keynote 自己的 HTML 导出会把 `FromDarkness` / `LineDraw` / `wipe` **降级成普通 `apple:dissolve`**（见应用包里的 `kpfAnimationFallbacks.plist`）。从 Apple 的参数定义重建，比 Apple 自己的导出更接近你的原稿。\n\n**build 不是单条缓动。** 每个 Keynote build 都会过冲再回落，每段还有各自的曲线——这才是「像 Keynote」而不是「像 CSS」的原因：\n\n| 效果 | 实测路径 |\n|---|---|\n| `zoom-big` | `10 → 1.08 @62% → 0.95 @82% → 1` |\n| `pop` | `0.6 → 1.125 @43% → 0.952 @66.7% → 1` |\n| `cube` | `−90° → −6.02° @52% → +2.86° @60% → 0°` |\n| `clothesline` | 五段衰减回弹 `0.31 / −0.21 / 0.07 / −0.014 / 0.0017` |\n\n用 `python3 tools/keynote-params.py <效果名>` 可以查任意效果的原始参数和曲线。\n\n**画线**只能用在带描边的 SVG `<path>` / `<line>` 上。笔尖和起点圆点建在该 path 自己的 `<svg>` 里，**动画结束后自动移除**，静止 DOM 仍是一条完整实线（截图 / PDF / PPTX 导出干净）。路径只采样一次并缓存，resize 或改 viewBox 会自动失效重建，笔尖不会飘。\n\n**键盘**的节奏是**有种子的伪随机**，不是真随机——重播时打字节奏完全一致（Keynote 也是确定性的，真随机没法做回归）。用 `data-peg-seed` 改种子；`data-peg-cursor=\"off\"` 关光标并把每段钳到 0.05–2.5s。光标宽度是**行高的 3%**，闪烁半周期 0.4s。\n\n方向用 `data-peg-direction=\"top|bottom|left|right\"`，位移量用 `data-peg-distance`。\n\n**逐字 / 逐词投递。** 加 `data-peg-delivery=\"character\"`（或 `\"word\"`）：\n\n```html\n<h1 data-peg-animate=\"fade-move\" data-peg-delivery=\"character\">逐字淡入并移动。</h1>\n```\n\n用 `Intl.Segmenter` 拆分，所以完整的字素簇不会被拆坏——家庭 emoji `👨‍👩‍👧‍👦`、肤色修饰 `👍🏽`、国旗、组合音标都安全；`word` 模式也走分词，不会把整句中文当成一个词。\n\n**节奏是「铺开」而不是固定每字延迟**：一行字无论多长都在 `duration` 内落完。固定延迟会让长句拖到好几秒。用 `data-peg-spread` 调投递窗口，或 `data-peg-letter-stagger` 强制固定间隔。\n\n> Keynote 真正的逐字节奏取不到——它的 HTML 导出把文字 build 栅格化成了图片序列。这个铺开模型是 Peg 的设计决定，不是 Keynote 实测值。\n\n**富文本不拆。** 元素里有子标签（`<strong>`、`<a>`…）时保持原结构、整体做动画并给出警告。拆出来的 span 带 `aria-hidden`，父元素补 `aria-label`，读屏只念一遍。\n\n**页内逐步构建。** 给需要「点一下才出现」的元素加 `data-peg-step=\"N\"`：方向键先在页内推进 build，走完才翻页；`←` 回退一步。\n\n- 不写或写错 → 当作 `0`（进页即播），写错会警告\n- 序号可以跳号：`1, 3, 7` 会压缩成三个连续的逻辑 step\n- 同一 step 的元素**同时**出现；元素内部的逐字节奏各自独立\n- 回退是瞬时复原该 step 的前态，前面的 step 保持终态——倒放动画看起来像 bug，所以不做\n- 未出现的内容带 `visibility: hidden` + `pointer-events: none`，不会被点到、被 focus、被读屏念到\n- 数字键 / 缩略图跳页、以及往回翻到之前的页，都直接落终态\n\n**Action（页内动作）：** `data-peg-action=\"move|scale|opacity\"`。一个元素只能带一个 action，要连着做就用 `data-peg-target` 让多个指令元素指向同一个目标——指令元素本身不渲染，引擎会**累加**状态（step 2 的缩放保留 step 1 的位移）：\n\n```html\n<div class=\"dot\" id=\"d\"></div>\n<i data-peg-action=\"move\"    data-peg-target=\"#d\" data-peg-x=\"700\" data-peg-step=\"1\"></i>\n<i data-peg-action=\"scale\"   data-peg-target=\"#d\" data-peg-scale=\"1.8\" data-peg-step=\"2\"></i>\n<i data-peg-action=\"opacity\" data-peg-target=\"#d\" data-peg-opacity=\"0.25\" data-peg-step=\"3\"></i>\n```\n\n**你自己写的 transform 不会被吃掉。** 元素上已有的 `rotate(-3deg)`、`translateX(-50%)` 会在引擎接手前被捕获、作为前缀参与合成。注意它只在编译期捕获一次，之后随断点变化的 transform 不会被跟进。\n\n**Magic Move（神奇移动）—— `data-peg-transition=\"magic-move\"`。** 你三个 deck 里用得第二多的效果（70 次，PianoTuner 48 页里有 28 页在用）。两页共有的对象会从原位置「走」到新位置，其余内容交叉淡入淡出。用 `data-peg-id` 配对：\n\n```html\n<div class=\"version\" data-peg-id=\"ver\">V1.0</div>\n<!-- 下一页 -->\n<section data-peg-transition=\"magic-move\">\n  <div class=\"version\" data-peg-id=\"ver\">Now</div>   <!-- 原地形变 -->\n</section>\n```\n\n**让它像苹果而不像杂耍的那条规则：会「走」的一起走，只是「出现」的才错开。** 配对上的对象**同时**移动——眼睛才能读成一个连贯的空间事件，错开就是混乱；只有没有前一位置的新内容才依次淡入（自动推导，每项上限 150ms、总计 600ms —— 150ms 是从 apple.com 的 `StaggeredFadeIn` 组件里量出来的苹果自己的值）。\n\n字号变化的文字走 **`font-size` 插值**而不是 transform 缩放，笔画不会糊也不会变粗。位置用中心对齐的 FLIP，你自己写的 transform 依然保留。\n\n配对只认显式的 `data-peg-id`——配错比不配更糟。要按文字内容配可以在该页加 `data-peg-magic-match=\"text\"` 单独打开。\n\n**总结模块合并页（苹果风格结尾）。** 这一页靠的就是 Magic Move：实测你的原稿里那一页**一个 build 都没有**——整个模块拼贴全是靠转场进来的，中间的主体锁定不动，其余模块在它周围淡入。所以不需要新功能：给中间元素一个和上一页共享的 `data-peg-id`，模块摆在四周，转场设成 `magic-move` 即可。见 `motion-library-demo.html` 最后一页。\n\n版式建议（来自对苹果公开总结页的分析）：**最多 ~12 块**，只留一个 hero；层级用**色块面积**而不是字号来表达；数字要是全页最响的东西（标签应该可以被忽略）；只用一个强调色，其余全部单色。\n\n打开 `motion-library-demo.html` 可以逐页试所有效果。\n\n**线条 / 波形 / 流程图的丝滑动效。** 给**实线** SVG `<path>`/`<line>`(不要给本就虚线的)加 `data-peg-animate=\"draw\"`，它会沿自身路径**从左到右扫出**(`stroke-dashoffset`，时长跟随 `--dur-draw`)——用于折线、波形、图表曲线、流程连线;是一次性的扫入,不是无限循环(无限循环是廉价感来源)。**让流程/路程图从左到右依次出现**:基线/连线用 `draw`,节点用 `data-peg-stagger`(父 `<g>`)+ `data-peg-animate=\"fade-up\"`(每个节点)配合递增 `data-peg-delay`。静止 DOM 始终是完整实线,截图 / PDF 导出显示成品。\n\n**多幕配色:** 给每个 `<section>` 设 `--accent: var(--topic-…)`(见上方主题色表),该页所有强调元素(关键词、`.evi`、图表线)自动跟随;低饱和、每页一种、同页一致。**图文同页**用 `.headline.with-fig` 把标题降一档;**图下结论**用 `.summary` 居中放在图的下方做引导;**`.plain`(大白话行)** 放在标题下、带主题色竖条,给非专业观众一句人话——幻灯片保留技术内容,这行只让所有人都看懂(加粗最关键那半句;图文同页要限 `max-width` 到文字栏;两行标题时注意 `top` 别压住)。样本见 `slides/` 模板 11。**`.compare`(前后对比表)** 每行「过去的局限 → 现在怎么做」,是给非专业观众**一眼看懂\"迭代了什么\"**最有效的形式;`.badge-first` 给真正\"行业首创\"的行打徽标(少用、且要有依据)。样本见模板 12。\n\n```html\n<div class=\"headline\" data-peg-animate=\"text-flip\">A calm upward reveal.</div>\n```\n\n打开 `letter-spring-demo.html` 可预览逐字标题动效。\n\n数据柱、横条、进度条可加 `data-peg-bar`，或直接使用 `.bar-fill` / `.progress-fill`。横向默认从左到右增长；竖向使用 `data-peg-bar=\"vertical\"`，从底部向上增长。\n\n```html\n<div class=\"bar-fill\" style=\"width:72%;\"></div>\n<div data-peg-bar=\"vertical\" style=\"height:180px;\"></div>\n```\n\n可用 `data-peg-bar-duration`、`data-peg-bar-delay`、`data-peg-bar-easing` 单独调整时长、延迟和曲线。DOM 保持最终数值，截图、PDF、PPTX 导出不显示半成品。\n\n页面切换可在 `<section>` 上添加 `data-peg-transition=\"dissolve\"`、`data-peg-transition=\"clothesline\"`（`apple:ClotheslinePush`，摆动+下坠，方向跟随前进/后退）、`data-peg-transition=\"push-left\"` 或 `data-peg-transition=\"text-flip\"`；`text-flip` 会让旧画面向上翻出，同时新画面从下方翻入，两个动作在同一时间轴上连贯进行。不设置时为默认瞬切。切换动效尊重 `prefers-reduced-motion`，并在 `noscale` 导出模式下关闭。打开 `transition-demo.html` 可对比 cut、dissolve、push-left、text-flip。\n\n```html\n<section class=\"paper\" data-label=\"Title\" data-peg-transition=\"text-flip\">...</section>\n```\n\n---\n\n### 中文排版\n\nPeg 双语可用。拉丁字体(Inter)为强负字距 + 紧凑行高而调;中文恰好相反——方块字在负字距下会相互挤撞、在紧行高下多行会发闷。`tokens/cjk.css` **仅在文档语言为中文时**重新调校排版 token,英文 deck 不受影响。\n\n- 在 `<html>` 上设 `lang=\"zh-CN\"`(或任意 `zh-…`),中文排版层自动生效:字距放松趋近 0,显示行高打开(`--lh-tight` 1.12 / `--lh-snug` 1.20 / `--lh-body` 1.7)。\n- 汉字用 **Noto Sans SC** 渲染,拉丁字母/数字仍用 **Inter**(逐字回退),所以「重写 ESP-IDF」这种中英混排也对。\n- 中文 deck 里若有纯拉丁元素(如品牌字标),给它加 `.latin` 类即可单独恢复 Inter 的紧字距。\n- 离线/换机/导 PDF 的中文 deck:把字体随 deck 打包(或用 `pyftsubset` 子集化到实际用字),**不要**依赖系统字体。\n\n### 字体与许可\n\n设计系统的**代码是 MIT**;但随附字体**不是 MIT**——它们以 **SIL 开源字体许可 1.1(OFL)** 发布,并按 OFL 要求随附了许可证全文:\n\n| 字体 | 用途 | 许可 | 来源 |\n|---|---|---|---|\n| **Inter**(Rasmus Andersson) | 拉丁 / 数字 | SIL OFL 1.1 — [`assets/Inter-OFL.txt`](assets/Inter-OFL.txt) | github.com/rsms/inter |\n| **Noto Sans SC**(Google) | 简体中文 | SIL OFL 1.1 — [`assets/NotoSansSC-OFL.txt`](assets/NotoSansSC-OFL.txt) | fonts.google.com/noto |\n\nOFL 允许自由使用、嵌入、再分发(含商用),前提是**许可证文本随字体一同保留**、且不得单独售卖字体本身。再分发时请把 `*-OFL.txt` 与 `assets/*.ttf` 一起带上。\n\n> 仓库体积提示:`assets/NotoSansSC.ttf` 是完整可变字体(~17 MB);打包成 deck 时请子集化到实际用字以保持精简。\n\n### 许可证\n\n**代码与设计 token:** MIT — 可自由用于商业及个人项目。\n**随附字体(`assets/*.ttf`):** SIL OFL 1.1(见上方「字体与许可」)。\n"}