feat: give the chapters motion, diagrams, and retrieval practice
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:
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 →
|
||||
|
||||
Reference in New Issue
Block a user