feat: give the chapters motion, diagrams, and retrieval practice
verify-and-publish / gate (push) Successful in 7m4s
verify-and-publish / publish (push) Has been skipped

Three strands of work on the chapter surface, all reading from the same
constraint: this site ships no runtime dependencies, so every effect below
is native CSS or an .astro component.

Motion (src/styles/motion.css, DrawRule.astro). A scroll-driven layer of
reveals, hero parallax, section depth, and a hairline that paints itself
along its path, scrubbed by `animation-timeline: view()` — 0KB against
lottie-web's ~60KB gzipped on the main thread. Every scrubbed rule sits
inside `prefers-reduced-motion: no-preference` and `@supports`, so a
Firefox reader or an opted-out one gets the complete static page rather
than one with holes in it. Ranges key to `cover 40%`-`cover 75%` where the
motion is meant to be watched: `view()` ranges key to first visibility,
which on a 2600px page is long before anyone is reading the section.
`.agents/skills/motion/references/scroll-driven.md` records the technique
and the two ways `pathLength` normalisation was broken while building it.

Diagrams (StepFlow.astro, WorktreeMap.astro). The `.steps` stack on
/skills/, /models/, and /agents/ becomes a numbered flow with connectors,
and /agents/ grows the worktree map it was describing in prose — reusing
the `trees` collection rather than a second set of strings. The map's
static variant is gated one class deeper than full-guide's page styles, so
the interactive copy renders byte-identical. Connectors are pseudo-elements,
not SVG: a stretched path desynchronises its own dash pattern, and a
straight line does not need one. Mermaid was considered and rejected at
~1MB of runtime.

Retrieval practice (/rules/, /skills/). A "check yourself" section of
native `<details>` question/answer pairs plus a citation row, both bilingual
through the existing `data-copy` toggle, and both JavaScript-free.

Two audit blind spots surfaced and are closed rather than worked around:
`build.inlineStylesheets: 'never'`, because Astro inlined sheets under ~4kB
and audit-ui.mjs reads its colour and size baseline from dist/_astro/*.css;
and `--columns` declared in the grid components, because an element-level
custom property is not a declaration the audit can resolve.

legacy/styles/skills.css is renamed and imported for its side effect. Inside
a *page*, `?url` resolves to that page's own CSS chunk whatever file it
names, so the link pointed at the wrong asset and the sheet was emitted but
never loaded — the package preview had been rendering unstyled and
overflowing since the Astro cutover.

verify.mjs gains 5 assertions for the recall sections and their Portuguese
copy: 89 now, against the 84 baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Marcos Paulo
2026-09-06 16:04:08 -03:00
parent ab2308441c
commit 79920c9e6c
38 changed files with 1712 additions and 133 deletions
+8 -2
View File
@@ -27,8 +27,14 @@ files — the pages listed above import them, and the build fails without them.
- **`legacy/styles/audit.css`** (was `full-guide/audit.css`) — responsive audit
overrides, imported by `full-guide.astro`.
- **`legacy/styles/chapters.css`** — imported by `ChapterLayout.astro`.
- **`legacy/styles/skills.css`**, **`skills-review.css`**, **`change-lens.css`**
— imported by their respective pages.
- **`legacy/styles/skills-chapter.css`**, **`skills-review.css`**,
**`change-lens.css`** — imported by their respective pages.
`skills-chapter.css` is imported for its side effect, not through the `?url` +
`<link slot="styles">` pattern the layout uses: inside a **page**, `?url` on a
stylesheet resolves to that page's own CSS chunk rather than to the imported
file, so the link points at the wrong asset and the sheet is emitted but never
loaded. It was named `skills.css` and silently unloaded that way until
2026-09-06.
- **`legacy/skills-review/`** — `app.js` and the module graph under it
(`catalog.js`, `submitted-catalog.js`, `files.js`, `submitted-files.js`,
`vote.js`). `catalog.js` + `submitted-catalog.js` are the review desk's real
+1 -1
View File
@@ -21,7 +21,7 @@ a bug.
```bash
python3 - <<'PY'
import re
files=['legacy/styles/guide.css','legacy/styles/chapters.css','legacy/styles/skills.css',
files=['legacy/styles/guide.css','legacy/styles/chapters.css','legacy/styles/skills-chapter.css',
'legacy/styles/skills-review.css','legacy/styles/change-lens.css',
'legacy/styles/audit.css','public/hands-on/starter/styles.css',
'public/hands-on/rules/styles.css']
+4
View File
@@ -41,6 +41,10 @@ If there is no answer, ship it static. That is a legitimate, common outcome.
- One thing moves at a time. No staggered card cascades.
- `will-change` only immediately before animating, removed after.
- No animation library. This site's thesis is having no runtime dependencies.
- Scroll-scrubbed motion (reveals, parallax, depth, line draw) has its own rules
— ranges, feature gates, the `linear` exception, SVG line drawing. Read
[`references/scroll-driven.md`](references/scroll-driven.md) before touching
`src/styles/motion.css`.
## Reduced motion is not optional
@@ -0,0 +1,188 @@
# Reference: scroll-driven motion
Everything scrubbed by scroll position on this site — reveals, parallax, section
depth, line draw. Read before adding a class to `src/styles/motion.css` or
reaching for a scroll library.
## Why this is the whole toolkit
`.agents/rules/animation.md` forbids animation libraries: the site's thesis is
having no runtime dependencies. That rules out Lottie, Rive, GSAP ScrollTrigger,
Motion One, and AOS. It is not a hardship — for the motion this site does, the
native features are also the better engineering:
| Approach | Runtime | Thread |
| ---------------------------- | --------- | ----------------------------- |
| `animation-timeline: view()` | 0 KB | compositor |
| lottie-web | ~60 KB gz | main, `requestAnimationFrame` |
| dotLottie | ~50 KB gz | main |
| Rive (WASM) | ~200 KB | main + WASM |
Lottie earns its place for cinematic, multi-layer, path-morphing artwork drawn
by a motion designer. Reveals, drifts, and a hairline drawing itself are not
that. If a future request genuinely needs frame-by-frame artwork, it is a rule
change to argue for in `.agents/rules/animation.md`, not a quiet `pnpm add`.
## Support and the two gates
Chromium 115+, Safari 26+. Firefox has it behind a flag. That is fine, because
every scrubbed rule sits inside two nested gates:
```css
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
/* scrubbed rules only */
}
}
```
The `@supports` gate is not decoration. Anything that hides an element at rest —
`opacity: 0`, `stroke-dashoffset: 100` — must live **inside** it. Declared
outside, a Firefox reader gets a permanently invisible element, because the
animation that would have revealed it never runs.
## Ranges
`animation-range` is the duration; there is no `animation-duration` on a
scrubbed animation. Defaults are not what you would guess: bare
`animation-range: normal` means `cover 0% cover 100%`, edge-to-edge for the
element's whole visible life, which is rarely the effect wanted. Always state
the range.
- `cover` — first pixel enters → last pixel leaves. The full journey.
- `contain` — only while fully visible. **Flips meaning** when the element is
taller than the viewport: it then means "the element fills the scrollport".
This is the usual source of confusion; prefer `entry`/`exit`/`cover`.
- `entry` — starts entering → fully inside. Use for one-shot arrivals.
- `exit` — starts leaving → fully outside. Use for departures.
Ranges may be mixed at the two ends (`entry 15% cover 55%`). Legal, and harder
to debug — do it only when a single named range cannot express the moment.
Always `both` for fill mode. Without it the animation snaps back to its `from`
state when scrolled past, and holds nothing before the range begins.
Always `linear` easing. The scroll position _is_ the timing function; a curve
here double-eases and reads as drag. This is the one sanctioned exception to
"never `linear`" in `.agents/rules/animation.md`; time-based motion still uses
`--ease-out`.
### Ranges key to first visibility, not to reading position
The trap that produced the first version of every animation on `/skills/`.
`entry` begins the moment one pixel crosses the bottom of the scrollport. On a
2600px page, a section's `entry` phase is over while the reader is still on the
hero — the motion is finished before anyone is looking at it. Measured on the
skills chapter: an `entry 15% cover 55%` line draw ran from scrollY 190 to 654,
and the section it belonged to did not reach the top of the viewport until 599.
For motion meant to be _watched_, target the band where the subject sits around
the middle of the screen: **`cover 40%` to `cover 75%`**. For motion meant to be
_over before reading starts_ — a reveal — `entry` is right, and that is why
`.reveal` keeps it.
Check this, do not eyeball it. Print
`getAnimations()[0].effect.getComputedTiming().progress` at several
`window.scrollTo` positions and confirm progress crosses 0→1 inside the range
where the section's own top is between roughly 0 and 400px.
### Amplitude has a floor
A drift spread across an element's whole visible life is mostly off-screen. A
22px/6px parallax pair measured out at 3-10px of separation across the window
where the section was actually read — arithmetically present, perceptually
absent. Motion that cannot be seen is not restraint, it is dead code. The pair
is 38px/10px for that reason, ~25px of separation in the reading window.
## What this site's classes mean
| Class | Range | For |
| ----------------------------- | --------------------- | ------------------------------------------ |
| `.reveal` | `entry 0% entry 45%` | a section rising into place, settled early |
| `.parallax-near` / `-far` | `exit 0% exit 100%` | hero pieces, which start already on screen |
| `.depth-slow` / `.depth-fast` | `entry 0% exit 100%` | the two columns of a mid-page section |
| `.draw-line path` | `cover 40% cover 75%` | a hairline painting itself, while watched |
The hero pair and the section pair differ in range for a reason: a hero is on
screen at load, so only its exit is watched; a mid-page section is watched from
first pixel to last.
Parallax rates go the **same** direction with different magnitudes. Opposite
directions read as the page coming apart, not as depth.
## Line draw
```css
/* inside both gates */
.draw-line path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: line-draw linear both;
animation-timeline: view();
animation-range: entry 15% cover 55%;
}
@keyframes line-draw {
to {
stroke-dashoffset: 0;
}
}
```
`pathLength="100"` normalises the path to 100 user units whatever its real
geometry, so the dash numbers above are constant with no JS measurement step.
`fill: none` on the path, or the shape fills in behind the stroke.
`stroke-dasharray` and `stroke-dashoffset` must be **equal** at rest. Offset
smaller than the array → the line starts part-drawn. Larger → a gap that never
closes.
### Never let user space and device space diverge
`pathLength` normalises in **user space**. Anything that makes the rendered
geometry a different shape from the user-space geometry desynchronises the dash
pattern, and the line renders as a painted run, a gap, and a floating fragment.
Two ways to cause it, both hit while building `DrawRule.astro`:
- `preserveAspectRatio="none"` on a stretched viewBox — each axis scales by a
different factor.
- `vector-effect="non-scaling-stroke"` — Chromium spaces dashes in device space
while `pathLength` normalises in user space. Uniform scaling does not save you
here.
The fix is to remove the scaling rather than compensate for it: build the
viewBox at the rendered pixel size and emit the path in those units. Scale
factor 1, one space, `stroke-width: 1` is 1px, and neither attribute is needed.
Verify by counting painted runs, not by looking — a correct draw has exactly one
at every progress value. Sample points along `path.getTotalLength()`, convert
the computed `stroke-dasharray`/`stroke-dashoffset` out of the normalised 100
units, and count transitions from gap to dash.
`stroke-dashoffset` is the site's one sanctioned exception to
transform/opacity-only. It forces a repaint, not a layout, and the subject is a
hairline — but do not extend the exception to filled artwork.
## Straight lines do not need SVG
A plain vertical or horizontal rule that grows is `transform: scaleY()` with
`transform-origin: top` on a 1px div. Fully composited, no repaint, no SVG.
Reach for the dash technique only when the path bends.
## Printing
Print freezes animations at their current time, so a section that never entered
prints invisible and a half-drawn line prints as a fragment. Every scrubbed
class needs an entry in the `@media print` block at the foot of `motion.css`
`animation: none` for transforms, plus `stroke-dasharray: none` for a drawn
line, since undoing the animation alone leaves the dash pattern in place.
## Checking it
- Chrome DevTools → **Rendering → Emulate `prefers-reduced-motion: reduce`**.
The static page must be correct and complete, not a page with holes in it.
- Firefox, or Chrome with `about:config`-style support disabled: same test for
the `@supports` fallback.
- `scroll-driven-animations.style` has a range visualiser; there is a
Scroll-Driven Animations debugger extension for DevTools.
- `bash .agents/scripts/gate.sh``audit-ui.mjs` fails the build on horizontal
overflow, which a mis-sized decorative SVG will cause.
+16 -1
View File
@@ -12,7 +12,22 @@ Split at
the
seam.
MAIN / ORCHESTRATOR
├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence
├── agent/ui → components + visual states
├── agent/tests → acceptance + regressions
└── agent/docs → guide + examples
merge after each leaf returns a diff and evidence
Orchestrator
main
● clean
UI worker
agent/ui
● working
Test worker
agent/tests
● ready
Docs worker
agent/docs
● review
FRAME
Orchestrator
Owns scope, task graph, boundaries, and integration.
+3 -1
View File
@@ -19,7 +19,9 @@ Two knobs
Capability
× effort
ROUTING RULE
strong model + high effort → frame ambiguity · light model + low effort → bounded execution · raise one knob at a time → compare evidence
strong model + high effort → frame ambiguity
light model + low effort → bounded execution
raise one knob at a time → compare evidence
Sequence
Spend judgment
where it
+17
View File
@@ -4,6 +4,7 @@ field guide
Pipeline
Skills
Examples
Recall
EN
/
PT
@@ -94,6 +95,22 @@ A second reader checks intent.
├── languages
└── instructions
Read review policy →
Retrieval practice
answer before you open
Desirable difficulty
Close the
page.
Recall.
Retrieval practice — recalling an answer from memory before re-reading — is what builds long-term retention. Answer each question from memory first, then open it to check.
Where does a rule belong: context, skill, CLI check, or hook?
+
Guidance the agent must discover goes in AGENTS.md or a skill. Deterministic policy becomes a CLI command, cheap gates run at commit time, and judgment calls go to review.
Why does a skill need a trigger, not just a workflow?
+
The trigger says when to load it. Without one the skill either never fires or loads every time — and a skill that always loads is just a slower prompt.
What separates a repository rule from a prompt?
+
The prompt is advice for one run. The rule is reusable context plus an executable boundary — still present when the conversation is gone.
COPY / ADAPT
Ask your agent to map the enforcement stack.
Use this in the interview repository or adapt the path names to another project.
+16
View File
@@ -39,6 +39,22 @@ Use references for facts and scripts for deterministic mechanics.
04
Evaluate behavior
Test realistic prompts, edge cases, safety, and evidence.
Check yourself
Recall it before
you
ship it.
Answer from memory first — the reveal is the feedback.
Format specification ↗
Try it on the Tiny Tasks lab →
The skill never loads. What is the first suspect?
+
The trigger. A precise description says when to load the skill — and when to leave it out. A vague one never fires.
Where do the workflow, the facts, and the repeated mechanics each go?
+
The workflow stays in SKILL.md, conditional facts move to references/, and deterministic repeated mechanics become scripts/.
What proves a skill works?
+
Behavior, not headings: realistic prompts, edge cases, and safety checks with observable evidence — the same bar the review desk applies.
Agents & trees →
Rules case study →
Review submitted skills →