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 →
|
||||
|
||||
@@ -3,4 +3,12 @@ import { defineConfig } from 'astro/config';
|
||||
export default defineConfig({
|
||||
base: '/ai-for-dummies',
|
||||
trailingSlash: 'always',
|
||||
build: {
|
||||
// Astro's default ('auto') inlines any stylesheet under ~4kB into a
|
||||
// <style> in the page. scripts/audit-ui.mjs reads its colour, size, and
|
||||
// breakpoint baseline from dist/_astro/*.css, so an inlined sheet drops
|
||||
// out of that audit entirely. Emitting every stylesheet as a file keeps
|
||||
// the whole cascade inside the audit's reach.
|
||||
inlineStylesheets: 'never',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
:root{--ink:#122534;--paper:#f6f3ed;--line:#d0d5d2;--muted:#65717a;--blue:#215675;--gold:#ebbf58;--red:#a7483f;--font-sans:manrope,arial,sans-serif;--font-mono:'DM Mono',monospace;}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--paper);font:16px/1.6 var(--font-sans)}main{max-width:1400px;margin:auto;padding:0 5vw}.top{display:flex;justify-content:space-between;gap:20px;padding:24px 0;border-bottom:1px solid var(--line);font:700 11px var(--font-mono);letter-spacing:.08em;text-transform:uppercase}.top a{color:var(--ink);text-decoration:none}.hero{padding:100px 0 70px;max-width:950px}.eyebrow{color:var(--red);font:700 11px var(--font-mono);letter-spacing:.12em;text-transform:uppercase}.hero h1{margin:16px 0;font-size:clamp(52px,9vw,126px);line-height:.9;letter-spacing:-.07em}.hero h1 em,h2 em{font:400 .9em Georgia,serif;color:var(--red)}.hero p{max-width:680px;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--line);border:1px solid var(--line);margin-bottom:100px}.card{min-height:220px;padding:28px;background:var(--paper)}.card b{color:var(--red);font:24px var(--font-mono)}.card h2{margin:18px 0 8px;font-size:25px;letter-spacing:-.04em}.card p{margin:0 0 14px;color:var(--muted)}.card a{color:var(--blue);font-weight:700}.model,.pipeline,.practice{display:grid;grid-template-columns:1fr 2fr;gap:50px;padding:80px 0;border-top:1px solid var(--line)}.model h2,.pipeline h2,.practice h2{margin:0;font-size:clamp(34px,5vw,70px);line-height:.95;letter-spacing:-.06em}.panel{padding:28px;background:var(--ink);color:var(--paper)}.panel strong{display:block;color:var(--gold);font:700 12px var(--font-mono);letter-spacing:.1em}.panel code{display:block;margin-top:18px;color:#d6e1e4;font:14px/1.8 var(--font-mono);white-space:pre-wrap}.steps{display:grid;gap:1px;background:var(--line)}.steps article{display:grid;grid-template-columns:70px 1fr;gap:20px;padding:20px;background:var(--paper)}.steps b{color:var(--red);font:20px var(--font-mono)}.steps strong{display:block}.steps span{color:var(--muted)}.links{display:flex;flex-wrap:wrap;gap:10px;margin:28px 0 70px}.links a{padding:10px 13px;color:var(--ink);border:1px solid var(--ink);text-decoration:none;font:11px var(--font-mono);text-transform:uppercase}.links a:hover{color:var(--paper);background:var(--ink)}footer{padding:30px 0 70px;color:var(--muted);font-size:13px}@media(max-width:800px){.grid,.model,.pipeline,.practice{grid-template-columns:1fr}.hero{padding:65px 0 45px}.model,.pipeline,.practice{gap:25px;padding:55px 0}}@media(max-width:520px){main{padding:0 16px}.top span{display:none}.hero h1{font-size:56px}.hero p{font-size:17px}.card{min-height:0}.steps article{grid-template-columns:45px 1fr}}
|
||||
:root{--ink:#122534;--paper:#f6f3ed;--line:#d0d5d2;--muted:#65717a;--blue:#215675;--gold:#ebbf58;--red:#a7483f;--font-sans:manrope,arial,sans-serif;--font-mono:'DM Mono',monospace;}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--paper);font:16px/1.6 var(--font-sans)}main{max-width:1400px;margin:auto;padding:0 5vw}.top{display:flex;justify-content:space-between;gap:20px;max-width:1400px;margin-inline:auto;padding:24px 5vw;border-bottom:1px solid var(--line);font:700 11px var(--font-mono);letter-spacing:.08em;text-transform:uppercase}.top a{color:var(--ink);text-decoration:none}.hero{padding:100px 0 70px;max-width:950px}.eyebrow{color:var(--red);font:700 11px var(--font-mono);letter-spacing:.12em;text-transform:uppercase}.hero h1{margin:16px 0;font-size:clamp(52px,9vw,126px);line-height:.9;letter-spacing:-.07em}.hero h1 em,h2 em{font:400 .9em Georgia,serif;color:var(--red)}.hero p{max-width:680px;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--line);border:1px solid var(--line);margin-bottom:100px}.card{min-height:220px;padding:28px;background:var(--paper)}.card b{color:var(--red);font:24px var(--font-mono)}.card h2{margin:18px 0 8px;font-size:25px;letter-spacing:-.04em}.card p{margin:0 0 14px;color:var(--muted)}.card a{color:var(--blue);font-weight:700}.model,.pipeline,.practice{display:grid;grid-template-columns:1fr 2fr;gap:50px;padding:80px 0;border-top:1px solid var(--line)}.model h2,.pipeline h2,.practice h2{margin:0;font-size:clamp(34px,5vw,70px);line-height:.95;letter-spacing:-.06em}.panel{padding:28px;background:var(--ink);color:var(--paper)}.panel strong{display:block;color:var(--gold);font:700 12px var(--font-mono);letter-spacing:.1em}.panel code{display:block;margin-top:18px;color:#d6e1e4;font:14px/1.8 var(--font-mono);white-space:pre-wrap}.steps{display:grid;gap:1px;background:var(--line)}.steps article{display:grid;grid-template-columns:70px 1fr;gap:20px;padding:20px;background:var(--paper)}.steps b{color:var(--red);font:20px var(--font-mono)}.steps strong{display:block}.steps span{color:var(--muted)}.links{display:flex;flex-wrap:wrap;gap:10px;margin:28px 0 70px}.links a{padding:10px 13px;color:var(--ink);border:1px solid var(--ink);text-decoration:none;font:11px var(--font-mono);text-transform:uppercase}.links a:hover{color:var(--paper);background:var(--ink)}footer{padding:30px 0 70px;color:var(--muted);font-size:13px}@media(max-width:800px){.grid,.model,.pipeline,.practice{grid-template-columns:1fr}.hero{padding:65px 0 45px}.model,.pipeline,.practice{gap:25px;padding:55px 0}}@media(max-width:520px){main{padding:0 16px}.top{padding-inline:16px}.top span{display:none}.hero h1{font-size:56px}.hero p{font-size:17px}.card{min-height:0}.steps article{grid-template-columns:45px 1fr}}
|
||||
|
||||
@@ -170,6 +170,17 @@ if (
|
||||
throw new Error('built rules page lost its accessible controls');
|
||||
if (!html.rules.includes('data-rules-copy') || !html.rules.includes('"pt"'))
|
||||
throw new Error('built rules page lost its Portuguese interaction data');
|
||||
if (
|
||||
!has(html.rules, [
|
||||
'id="recall"',
|
||||
'data-copy="recallQ1"',
|
||||
'data-copy="recallQ2"',
|
||||
'data-copy="recallQ3"',
|
||||
])
|
||||
)
|
||||
throw new Error('built rules page lost its retrieval-practice section');
|
||||
if (!html.rules.includes('Dificuldade desejável') || !html.rules.includes('"recallA2"'))
|
||||
throw new Error('built rules page lost its Portuguese recall copy');
|
||||
|
||||
if (
|
||||
!has(html.review, [
|
||||
@@ -233,6 +244,25 @@ if (
|
||||
])
|
||||
)
|
||||
throw new Error('built skills page lost its package explorer');
|
||||
if (
|
||||
!has(html.skills, [
|
||||
'id="recall"',
|
||||
'<summary',
|
||||
'The skill never loads. What is the first suspect?',
|
||||
'agentskills.io/specification',
|
||||
'hands-on/starter/',
|
||||
])
|
||||
)
|
||||
throw new Error('built skills page lost its recall section or citations');
|
||||
if (
|
||||
!has(read('src/content/chapters/skills.json'), [
|
||||
'"recall"',
|
||||
'"links"',
|
||||
'Teste-se',
|
||||
'Especificação do formato',
|
||||
])
|
||||
)
|
||||
throw new Error('skills chapter lost its Portuguese recall copy');
|
||||
if (!has(html.starter, ['id="task-list"', 'id="task-count"']))
|
||||
throw new Error('starter lab lost its task-list contract');
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
// The eyebrow reuses the existing `Eyebrow` primitive but defaults to the
|
||||
// `red` tone, which matches the chapter surfaces (not the guide surface).
|
||||
// The h1 em treatment (Georgia italic, red) is the chapter-page signature.
|
||||
//
|
||||
// The `rise-in` / `parallax-*` classes come from src/styles/motion.css,
|
||||
// loaded by ChapterLayout: the hero rises once on load, and the headline
|
||||
// and intro lag the scroll at two depths as the hero exits. Both are
|
||||
// inert without the motion layer or when the user opted out of motion.
|
||||
|
||||
import Eyebrow from '../primitives/Eyebrow.astro';
|
||||
|
||||
@@ -20,10 +25,10 @@ interface Props {
|
||||
const { eyebrow, tone = 'red' } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<section class="hero rise-in">
|
||||
<Eyebrow label={eyebrow} tone={tone} />
|
||||
<h1><slot name="title" /></h1>
|
||||
<div class="intro"><slot /></div>
|
||||
<h1 class="parallax-near"><slot name="title" /></h1>
|
||||
<div class="intro parallax-far"><slot /></div>
|
||||
<slot name="foot" />
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
// DrawRule — a hairline that paints itself along its own path as the section
|
||||
// containing it scrolls into view. The scrubbing lives in the shared motion
|
||||
// layer (src/styles/motion.css, `.draw-line`); this component only supplies
|
||||
// the geometry.
|
||||
//
|
||||
// Purely decorative: it marks reading position and ties a heading column to
|
||||
// the panel beside it. `aria-hidden` and no accessible name, because nothing
|
||||
// here carries information a reader could miss (.agents/rules/accessibility.md).
|
||||
//
|
||||
// Two shapes:
|
||||
// rule — a plain vertical hairline
|
||||
// bracket — the `└─` the skill-package tree already speaks in: down the
|
||||
// heading column, then a turn toward the panel
|
||||
//
|
||||
// `pathLength="100"` normalises the path to 100 units so motion.css can use
|
||||
// constant dash numbers with no JS measurement step. Two earlier versions of
|
||||
// this component broke that normalisation, both showing up the same way — the
|
||||
// hairline rendered as a drawn run, a gap, and a floating fragment:
|
||||
//
|
||||
// 1. `preserveAspectRatio="none"` stretching a square viewBox. The dash
|
||||
// pattern is laid out in user space, then stretched by a different
|
||||
// factor on each axis.
|
||||
// 2. `vector-effect="non-scaling-stroke"`. Chromium spaces dashes in device
|
||||
// space while `pathLength` normalises in user space.
|
||||
//
|
||||
// So the viewBox is built at the rendered pixel size and the path is emitted
|
||||
// in those units. User space and device space are then the same space, the
|
||||
// scale factor is 1, and the whole class of mismatch is gone — no
|
||||
// `preserveAspectRatio`, no `vector-effect`, `stroke-width: 1` is 1px.
|
||||
|
||||
interface Props {
|
||||
shape?: 'rule' | 'bracket';
|
||||
/** Rendered size of the SVG box, in CSS pixels. The bracket is square. */
|
||||
height?: number;
|
||||
/** Extra classes for the wrapping svg. */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { shape = 'rule', height = 120, class: className } = Astro.props;
|
||||
|
||||
// The bracket turns at 78% of the descent, so the corner sits clear of the
|
||||
// arm. The rule is the bare descent in a 4px-wide box, so it claims no width
|
||||
// it does not use. `+ 0.5` puts the 1px stroke on the pixel grid.
|
||||
const turn = Math.round(height * 0.78) + 0.5;
|
||||
const foot = Math.round(height * 0.99) + 0.5;
|
||||
const path =
|
||||
shape === 'bracket'
|
||||
? `M 0.5 0 V ${turn} Q 0.5 ${foot} ${Math.round(height * 0.22)} ${foot} H ${height}`
|
||||
: `M 0.5 0 V ${height}`;
|
||||
const viewBox = shape === 'bracket' ? `0 0 ${height} ${height}` : `0 0 4 ${height}`;
|
||||
---
|
||||
|
||||
<svg
|
||||
class:list={['draw-line', className]}
|
||||
viewBox={viewBox}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
style={`--draw-height: ${height}px`}
|
||||
>
|
||||
<path d={path} pathLength="100"></path>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.draw-line {
|
||||
/* Declared here rather than left to a `var()` fallback: audit-ui.mjs
|
||||
resolves every variable a page uses against the stylesheets it links,
|
||||
and a fallback does not count as a definition. The `height` prop sets
|
||||
this inline, which wins over this rule. */
|
||||
--draw-height: 120px;
|
||||
|
||||
display: block;
|
||||
/* Height drives the box; width follows from the viewBox aspect ratio.
|
||||
`max-width` is the guard for the one case that would break it — a
|
||||
column narrower than the glyph is tall. */
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: var(--draw-height);
|
||||
margin-top: 28px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.draw-line path {
|
||||
fill: none;
|
||||
stroke: var(--line);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* Below the chapter grid's single-column breakpoint the heading and the
|
||||
panel stack, so the bracket's arm would point at nothing. Shorten the
|
||||
box and let it read as a plain descent into the panel. */
|
||||
@media (max-width: 800px) {
|
||||
.draw-line {
|
||||
/* Overrides the inline `--draw-height` by winning on cascade order,
|
||||
not by `!important` — the inline style only sets the variable. */
|
||||
height: 48px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,12 +16,14 @@ interface Props {
|
||||
label: string;
|
||||
/** Number of columns at the widest breakpoint. */
|
||||
columns?: number;
|
||||
/** Extra classes for the section (e.g. `reveal` from the motion layer). */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { label, columns = 3 } = Astro.props;
|
||||
const { label, columns = 3, class: className } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="group" aria-label={label}>
|
||||
<section class:list={['group', className]} aria-label={label}>
|
||||
<div class="grid" style={`--columns: ${columns}`}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -29,6 +31,12 @@ const { label, columns = 3 } = Astro.props;
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
/* Declared here rather than left to the inline `style` alone:
|
||||
audit-ui.mjs resolves every variable a page uses against the
|
||||
stylesheets it links, and an element-level custom property is not a
|
||||
declaration it can see. The `columns` prop overrides this inline. */
|
||||
--columns: 3;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns), 1fr);
|
||||
gap: 1px; /* hairline separators, drawn by the parent background */
|
||||
|
||||
@@ -14,17 +14,25 @@
|
||||
interface Props {
|
||||
/** Number of columns at the widest breakpoint. */
|
||||
columns?: number;
|
||||
/** Extra classes for the section (e.g. `reveal` from the motion layer). */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { columns = 3 } = Astro.props;
|
||||
const { columns = 3, class: className } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="grid" style={`--columns: ${columns}`}>
|
||||
<section class:list={['grid', className]} style={`--columns: ${columns}`}>
|
||||
<slot />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
/* Declared here rather than left to the inline `style` alone:
|
||||
audit-ui.mjs resolves every variable a page uses against the
|
||||
stylesheets it links, and an element-level custom property is not a
|
||||
declaration it can see. The `columns` prop overrides this inline. */
|
||||
--columns: 3;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns), 1fr);
|
||||
gap: 1px; /* hairline separators, drawn by the parent background */
|
||||
|
||||
@@ -28,7 +28,12 @@ const { navLabel = 'Chapter navigation' } = Astro.props;
|
||||
|
||||
<style>
|
||||
.footer {
|
||||
padding: 30px 0 70px;
|
||||
/* Same column geometry as `main` in chapters.css — see TopBar.astro.
|
||||
The links block keeps its own rhythm inside the padded column. */
|
||||
max-width: 1400px;
|
||||
margin-inline: auto;
|
||||
padding-block: 30px 70px;
|
||||
padding-inline: 5vw;
|
||||
color: var(--muted);
|
||||
/* UNRESOLVED: legacy 13px fixed from chapters.css. No token matches.
|
||||
Reported in task 09 report. */
|
||||
@@ -49,6 +54,13 @@ const { navLabel = 'Chapter navigation' } = Astro.props;
|
||||
text-decoration: none;
|
||||
font: var(--step-0) var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
/* Hover colour changes and press feedback share one transition list —
|
||||
appending transform keeps the base link transition's spirit without
|
||||
clobbering it (base.css styles bare `a`, this is more specific). */
|
||||
transition:
|
||||
color var(--dur-press) var(--ease-out),
|
||||
background-color var(--dur-press) var(--ease-out),
|
||||
transform var(--dur-press) var(--ease-out);
|
||||
}
|
||||
|
||||
.links :global(a:hover) {
|
||||
@@ -60,4 +72,27 @@ const { navLabel = 'Chapter navigation' } = Astro.props;
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Press feedback: the pill settles 3% under the finger. Movement only
|
||||
when the user has not opted out; colour transitions stay for everyone. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.links :global(a:active) {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.links :global(a) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* chapters.css switches main to a 16px gutter at this legacy threshold;
|
||||
the footer switches with it so the column never splits. */
|
||||
/* token-gap: matches the legacy chapters.css main gutter switch at 520px; not a named breakpoint */
|
||||
@media (max-width: 520px) {
|
||||
.footer {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
// StepFlow — the numbered practice steps drawn as a flow diagram: boxed nodes
|
||||
// joined by a connector and an arrowhead, so the order reads as a sequence
|
||||
// rather than as three items that happen to be numbered.
|
||||
//
|
||||
// Replaces the `.steps` stack from legacy/styles/chapters.css on the chapter
|
||||
// pages. The rendered text is unchanged — same number, label, and copy in the
|
||||
// same order — because .agents/snapshots/*.txt pin it.
|
||||
//
|
||||
// The connector is a 1px pseudo-element and a border triangle, not an SVG.
|
||||
// A straight line does not need a path (see
|
||||
// .agents/skills/motion/references/scroll-driven.md), and the chain's height
|
||||
// depends on how the copy wraps, so anything scaled to fit would desynchronise
|
||||
// its own dash pattern. Motion for this section is the shared `.depth-*` drift
|
||||
// the page applies from outside.
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
copy: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
steps: Step[];
|
||||
/** Extra classes for the list, e.g. a `.depth-fast` drift from the page. */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { steps, class: className } = Astro.props;
|
||||
---
|
||||
|
||||
<ol class:list={['flow', className]}>
|
||||
{
|
||||
steps.map((step, index) => (
|
||||
<li>
|
||||
<article>
|
||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||
<div>
|
||||
<strong>{step.label}</strong>
|
||||
<span>{step.copy}</span>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
|
||||
<style>
|
||||
.flow {
|
||||
/* Declared here rather than left to a `var()` fallback: audit-ui.mjs
|
||||
resolves every variable a page uses against the stylesheets it links,
|
||||
and a fallback does not count as a definition. */
|
||||
--flow-gutter: 64px;
|
||||
--flow-pad: 20px;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.flow li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* The join lives on the second and later nodes, so the chain has no
|
||||
dangling stub at either end. */
|
||||
.flow li + li {
|
||||
padding-top: 34px;
|
||||
}
|
||||
|
||||
.flow li + li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/* Centre of the number column: the node's padding plus half the gutter. */
|
||||
left: calc(var(--flow-pad) + var(--flow-gutter) / 2);
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.flow li + li::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: calc(var(--flow-pad) + var(--flow-gutter) / 2 - 5px);
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
border-top: 8px solid var(--gold);
|
||||
}
|
||||
|
||||
.flow article {
|
||||
display: grid;
|
||||
grid-template-columns: var(--flow-gutter) 1fr;
|
||||
gap: 20px;
|
||||
padding: var(--flow-pad);
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.flow b {
|
||||
/* Centred in its column so the number sits directly over the connector
|
||||
that leaves the node below it. */
|
||||
text-align: center;
|
||||
color: var(--red);
|
||||
font: var(--step-20) / 1 var(--font-mono);
|
||||
}
|
||||
|
||||
.flow strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flow span {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.flow {
|
||||
--flow-gutter: 45px;
|
||||
--flow-pad: 16px;
|
||||
}
|
||||
.flow article {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -32,7 +32,14 @@ const { id } = Astro.props;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 0;
|
||||
/* Mirror the `main` column in chapters.css (max-width 1400px,
|
||||
padding 0 5vw) so the bar is the same column as the content: at wide
|
||||
viewports the header aligns with main instead of hugging the viewport
|
||||
edge, and the hairline spans the column. */
|
||||
max-width: 1400px;
|
||||
margin-inline: auto;
|
||||
padding-block: 24px;
|
||||
padding-inline: 5vw;
|
||||
border-bottom: 1px solid var(--line);
|
||||
font: 700 var(--step-0) var(--font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
@@ -61,4 +68,13 @@ const { id } = Astro.props;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* chapters.css switches main to a 16px gutter at this legacy threshold;
|
||||
the bar switches with it so the column never splits. */
|
||||
/* token-gap: matches the legacy chapters.css main gutter switch at 520px; not a named breakpoint */
|
||||
@media (max-width: 520px) {
|
||||
.top {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
// Static shell: the SVG paths render server-side; the nodes are buttons with
|
||||
// the `data-tree` hook asserted by `scripts/verify.mjs`. The `root` node and
|
||||
// the `initial` branch are marked selected.
|
||||
//
|
||||
// `interactive={false}` renders the same diagram as plain divs with no tree
|
||||
// semantics, for a page that ships no JS. A button that cannot be pressed and
|
||||
// a `role="treeitem"` that nothing can select are both worse than static text
|
||||
// (.agents/rules/accessibility.md); the node copy stays readable either way.
|
||||
|
||||
type Localized = { en: string; pt: string };
|
||||
|
||||
@@ -26,23 +31,37 @@ interface Props {
|
||||
initial?: string;
|
||||
rootLabel?: string | Localized;
|
||||
rootSmall?: string | Localized;
|
||||
/** False on pages with no selector island driving the nodes. */
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean' } = Astro.props;
|
||||
const {
|
||||
branches,
|
||||
initial = 'main',
|
||||
rootLabel = 'ROOT',
|
||||
rootSmall = '● clean',
|
||||
interactive = true,
|
||||
} = Astro.props;
|
||||
|
||||
const Node = interactive ? 'button' : 'div';
|
||||
---
|
||||
|
||||
<div class="tree-stage" role="tree" aria-label="Repository worktree topology">
|
||||
<div
|
||||
class:list={['tree-stage', { 'is-static': !interactive }]}
|
||||
role={interactive ? 'tree' : undefined}
|
||||
aria-label={interactive ? 'Repository worktree topology' : undefined}
|
||||
>
|
||||
<svg viewBox="0 0 760 330" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path class="tree-edge trunk" d="M380 48 V118"></path>
|
||||
<path class="tree-edge" d="M380 118 C380 170 110 150 110 224"></path>
|
||||
<path class="tree-edge" d="M380 118 V224"></path>
|
||||
<path class="tree-edge" d="M380 118 C380 170 650 150 650 224"></path>
|
||||
</svg>
|
||||
<button
|
||||
<Node
|
||||
class:list={['tree-node', 'root', { active: initial === 'main' }]}
|
||||
data-tree="main"
|
||||
role="treeitem"
|
||||
aria-selected={initial === 'main'}
|
||||
data-tree={interactive ? 'main' : undefined}
|
||||
role={interactive ? 'treeitem' : undefined}
|
||||
aria-selected={interactive ? initial === 'main' : undefined}
|
||||
>
|
||||
<span>
|
||||
{
|
||||
@@ -73,14 +92,14 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
||||
)
|
||||
}
|
||||
</small>
|
||||
</button>
|
||||
</Node>
|
||||
{
|
||||
branches.map((branch) => (
|
||||
<button
|
||||
<Node
|
||||
class:list={['tree-node', 'branch', branch.tone, { active: branch.id === initial }]}
|
||||
data-tree={branch.id}
|
||||
role="treeitem"
|
||||
aria-selected={branch.id === initial}
|
||||
data-tree={interactive ? branch.id : undefined}
|
||||
role={interactive ? 'treeitem' : undefined}
|
||||
aria-selected={interactive ? branch.id === initial : undefined}
|
||||
>
|
||||
<>
|
||||
<span>
|
||||
@@ -109,7 +128,7 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
||||
)}
|
||||
</small>
|
||||
</>
|
||||
</button>
|
||||
</Node>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@@ -135,6 +154,10 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
||||
fill: none;
|
||||
stroke: var(--gold);
|
||||
stroke-width: 2;
|
||||
/* The viewBox is stretched by `preserveAspectRatio="none"`, so without
|
||||
this a vertical edge is drawn thicker than a horizontal one by the
|
||||
width ratio. Safe here because nothing dashes these paths. */
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
/* Root sits at the top centre, spanning both columns. */
|
||||
@@ -190,6 +213,106 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
/* Static variant.
|
||||
|
||||
The interactive copy takes its geometry from the page styles in
|
||||
src/pages/full-guide.astro, which position the nodes absolutely over the
|
||||
SVG. The grid above is what is left when those styles are not present,
|
||||
and it does not line up with the edges. So the static variant carries
|
||||
its own stage: same absolute placement, tuned for a chapter page rather
|
||||
than the guide's dark control surface.
|
||||
|
||||
Every selector here is `.tree-stage.is-static`, one class more specific
|
||||
than anything full-guide.astro declares, so the interactive diagram is
|
||||
untouched no matter which stylesheet lands first. */
|
||||
.tree-stage.is-static {
|
||||
display: block;
|
||||
height: 330px;
|
||||
padding: 0;
|
||||
background: var(--deep);
|
||||
border: 1px solid var(--ink);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Nothing to press when the nodes are divs. */
|
||||
.tree-stage.is-static .tree-node {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Placement is gated on width rather than overridden below it: the node
|
||||
offsets are percentages of a stage wide enough to hold three across,
|
||||
and unwinding them one by one is a specificity fight. Under 800px the
|
||||
topology becomes a stack instead, and the edges — geometry for a layout
|
||||
that is gone — go with it. The `↓` markers in the narrow rules further
|
||||
down carry the direction. */
|
||||
@media (min-width: 800px) {
|
||||
.tree-stage.is-static .tree-node {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
gap: 6px;
|
||||
width: 164px;
|
||||
min-height: 0;
|
||||
padding: 13px 15px;
|
||||
}
|
||||
|
||||
.tree-stage.is-static .tree-node.root {
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
width: 164px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tree-stage.is-static .tree-node.branch {
|
||||
top: 220px;
|
||||
}
|
||||
|
||||
.tree-stage.is-static .tree-node.branch.ui {
|
||||
left: 3%;
|
||||
}
|
||||
|
||||
.tree-stage.is-static .tree-node.branch.tests {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tree-stage.is-static .tree-node.branch.docs {
|
||||
right: 3%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.tree-stage.is-static {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.tree-stage.is-static svg {
|
||||
display: none;
|
||||
}
|
||||
/* The tone classes place branches across the two-column grid, which
|
||||
does not exist here. */
|
||||
.tree-stage.is-static .tree-node,
|
||||
.tree-stage.is-static .tree-node.root,
|
||||
.tree-stage.is-static .tree-node.branch.ui,
|
||||
.tree-stage.is-static .tree-node.branch.tests,
|
||||
.tree-stage.is-static .tree-node.branch.docs {
|
||||
position: relative;
|
||||
grid-column: 1;
|
||||
justify-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
.tree-stage.is-static .tree-node:not(:last-child)::after {
|
||||
content: '↓';
|
||||
position: absolute;
|
||||
bottom: -26px;
|
||||
left: 50%;
|
||||
color: var(--gold);
|
||||
}
|
||||
}
|
||||
|
||||
/* Branch tone variants — left/right placement stays on the grid columns. */
|
||||
.tree-node.branch.ui {
|
||||
grid-column: 1;
|
||||
|
||||
@@ -42,6 +42,7 @@ import prompts from '../../content/rules/prompts.json';
|
||||
<a href="#pipeline" data-copy="navPipeline">{copy.en.navPipeline}</a>
|
||||
<a href="#skills" data-copy="navSkills">{copy.en.navSkills}</a>
|
||||
<a href="#examples" data-copy="navExamples">{copy.en.navExamples}</a>
|
||||
<a href="#recall" data-copy="navRecall">{copy.en.navRecall}</a>
|
||||
</nav>
|
||||
<div class="languages" aria-label="Language">
|
||||
<button class="active" data-lang="en" aria-pressed="true">EN</button>
|
||||
@@ -51,10 +52,10 @@ import prompts from '../../content/rules/prompts.json';
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<section class="hero rise-in">
|
||||
<p class="eyebrow" data-copy="heroEyebrow">{copy.en.heroEyebrow}</p>
|
||||
<h1 data-copy="heroTitle" set:html={copy.en.heroTitle} />
|
||||
<div class="hero-foot">
|
||||
<h1 class="parallax-near" data-copy="heroTitle" set:html={copy.en.heroTitle} />
|
||||
<div class="hero-foot parallax-far">
|
||||
<p data-copy="heroText">{copy.en.heroText}</p>
|
||||
<aside>
|
||||
<span>CASE / NETCRACKER</span>
|
||||
@@ -64,12 +65,12 @@ import prompts from '../../content/rules/prompts.json';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="thesis">
|
||||
<section class="thesis reveal">
|
||||
<span data-copy="thesisLabel">{copy.en.thesisLabel}</span>
|
||||
<strong data-copy="thesis">{copy.en.thesis}</strong>
|
||||
</section>
|
||||
|
||||
<section class="pipeline-section" id="pipeline">
|
||||
<section class="pipeline-section reveal" id="pipeline">
|
||||
<div class="section-label">
|
||||
<span data-copy="pipelineLabel">{copy.en.pipelineLabel}</span>
|
||||
<span data-copy="pipelineMeta">{copy.en.pipelineMeta}</span>
|
||||
@@ -115,7 +116,7 @@ import prompts from '../../content/rules/prompts.json';
|
||||
<article class="stage-detail" id="stage-detail" aria-live="polite"></article>
|
||||
</section>
|
||||
|
||||
<section class="skill-section" id="skills">
|
||||
<section class="skill-section reveal" id="skills">
|
||||
<div class="section-label">
|
||||
<span data-copy="skillsLabel">{copy.en.skillsLabel}</span>
|
||||
<span data-copy="skillsMeta">{copy.en.skillsMeta}</span>
|
||||
@@ -158,7 +159,7 @@ import prompts from '../../content/rules/prompts.json';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="examples" id="examples">
|
||||
<section class="examples reveal" id="examples">
|
||||
<div class="section-label">
|
||||
<span data-copy="examplesLabel">{copy.en.examplesLabel}</span>
|
||||
<span data-copy="examplesMeta">{copy.en.examplesMeta}</span>
|
||||
@@ -223,7 +224,47 @@ fix(api): scope session query</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="copy-lab">
|
||||
<!--
|
||||
Retrieval practice (teach-skill structure): recall from memory, then
|
||||
open for feedback. Native <details> — no JavaScript, keyboard-ready.
|
||||
The language toggle rewrites these via data-copy like every other
|
||||
string; recallTitle carries markup, so it is in HTML_KEYS below.
|
||||
-->
|
||||
<section class="recall reveal" id="recall">
|
||||
<div class="section-label">
|
||||
<span data-copy="recallLabel">{copy.en.recallLabel}</span>
|
||||
<span data-copy="recallMeta">{copy.en.recallMeta}</span>
|
||||
</div>
|
||||
<div class="intro">
|
||||
<div>
|
||||
<p class="eyebrow" data-copy="recallEyebrow">{copy.en.recallEyebrow}</p>
|
||||
<h2 data-copy="recallTitle" set:html={copy.en.recallTitle} />
|
||||
</div>
|
||||
<p data-copy="recallText">{copy.en.recallText}</p>
|
||||
</div>
|
||||
<div class="recall-grid">
|
||||
<details>
|
||||
<summary>
|
||||
<span data-copy="recallQ1">{copy.en.recallQ1}</span><b aria-hidden="true">+</b>
|
||||
</summary>
|
||||
<p data-copy="recallA1">{copy.en.recallA1}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<span data-copy="recallQ2">{copy.en.recallQ2}</span><b aria-hidden="true">+</b>
|
||||
</summary>
|
||||
<p data-copy="recallA2">{copy.en.recallA2}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<span data-copy="recallQ3">{copy.en.recallQ3}</span><b aria-hidden="true">+</b>
|
||||
</summary>
|
||||
<p data-copy="recallA3">{copy.en.recallA3}</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="copy-lab reveal">
|
||||
<div>
|
||||
<span data-copy="copyLabel">{copy.en.copyLabel}</span>
|
||||
<h2 data-copy="copyTitle">{copy.en.copyTitle}</h2>
|
||||
@@ -239,7 +280,7 @@ fix(api): scope session query</code></pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="deeper">
|
||||
<section class="deeper reveal">
|
||||
<div>
|
||||
<span data-copy="deeperLabel">{copy.en.deeperLabel}</span>
|
||||
<strong data-copy="deeperTitle">{copy.en.deeperTitle}</strong>
|
||||
@@ -413,6 +454,7 @@ fix(api): scope session query</code></pre>
|
||||
pipelineTitle: 1,
|
||||
skillsTitle: 1,
|
||||
ratchetTitle: 1,
|
||||
recallTitle: 1,
|
||||
navPipeline: 1,
|
||||
navSkills: 1,
|
||||
navExamples: 1,
|
||||
@@ -683,6 +725,7 @@ fix(api): scope session query</code></pre>
|
||||
.pipeline-section,
|
||||
.skill-section,
|
||||
.examples,
|
||||
.recall,
|
||||
.copy-lab {
|
||||
margin-bottom: 140px;
|
||||
padding-top: 30px;
|
||||
@@ -952,6 +995,73 @@ fix(api): scope session query</code></pre>
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
/* Retrieval-practice cards. Same hairline grid as the examples above;
|
||||
the question is the card face, the answer is one disclosure away. */
|
||||
.recall-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1px;
|
||||
margin-top: 45px;
|
||||
background: var(--line);
|
||||
}
|
||||
.recall-grid details {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
min-height: 210px;
|
||||
padding: 30px;
|
||||
background: var(--paper);
|
||||
}
|
||||
.recall-grid summary {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
/* Hide the UA triangle on both engines; the `+` marker carries the
|
||||
state instead and is decorative (aria-hidden). */
|
||||
list-style: none;
|
||||
}
|
||||
.recall-grid summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.recall-grid summary span {
|
||||
color: var(--ink);
|
||||
font-size: var(--step-16);
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.recall-grid summary b {
|
||||
color: var(--blue);
|
||||
font: 700 var(--step-16) var(--font-mono);
|
||||
transition: transform var(--dur-press) var(--ease-out);
|
||||
}
|
||||
/* `+` becomes `×`: the only state change the marker needs. */
|
||||
.recall-grid details[open] summary b {
|
||||
color: var(--gold);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.recall-grid details p {
|
||||
margin: 14px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: var(--step-14);
|
||||
line-height: 1.7;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.recall-grid details[open] p {
|
||||
/* purpose: make the revealed answer legible as a state change */
|
||||
animation: rules-detail-in var(--dur-rise) var(--ease-out) both;
|
||||
}
|
||||
|
||||
/* Press feedback: the console's click targets settle 3% under the
|
||||
finger (.agents/skills/improve-animations/AUDIT.md). Declared after
|
||||
the :hover lifts so an active press reads as pressed, not lifted. */
|
||||
.pipeline button:active,
|
||||
.skill-list button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-lab {
|
||||
display: grid;
|
||||
grid-template-columns: 0.75fr 1.25fr;
|
||||
@@ -998,6 +1108,15 @@ fix(api): scope session query</code></pre>
|
||||
600 9px 'DM Mono',
|
||||
monospace;
|
||||
cursor: pointer;
|
||||
/* Press feedback only — nothing else changes on interaction, so the
|
||||
list stays transform. */
|
||||
transition: transform var(--dur-press) var(--ease-out);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.copy-lab button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
.copy-lab pre {
|
||||
padding: 26px;
|
||||
@@ -1070,6 +1189,7 @@ fix(api): scope session query</code></pre>
|
||||
.pipeline button:focus-visible,
|
||||
.skill-list button:focus-visible,
|
||||
.copy-lab button:focus-visible,
|
||||
.recall-grid summary:focus-visible,
|
||||
.deeper a:focus-visible {
|
||||
outline: 3px solid var(--gold);
|
||||
outline-offset: -3px;
|
||||
@@ -1147,7 +1267,8 @@ fix(api): scope session query</code></pre>
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: none;
|
||||
}
|
||||
.example-grid {
|
||||
.example-grid,
|
||||
.recall-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.deeper nav {
|
||||
@@ -1240,7 +1361,9 @@ fix(api): scope session query</code></pre>
|
||||
transition: none;
|
||||
}
|
||||
.pipeline button,
|
||||
.skill-list button {
|
||||
.skill-list button,
|
||||
.copy-lab button,
|
||||
.recall-grid summary b {
|
||||
transition: none;
|
||||
}
|
||||
.stage-detail > *,
|
||||
|
||||
@@ -89,9 +89,24 @@ const packageFiles: PackageFile[] = [
|
||||
const preview = document.querySelector('#package-preview');
|
||||
const buttons = document.querySelectorAll('[data-skill-file]');
|
||||
const dataNode = document.querySelector('[data-skill-files]');
|
||||
const tree = document.querySelector('[data-package-workbench] .package-tree');
|
||||
if (!preview || !dataNode) return;
|
||||
const files = JSON.parse(dataNode.textContent || '[]');
|
||||
|
||||
// Position the gold selection bar over the active button. The bar is a
|
||||
// 1px-tall pseudo-element on the tree, moved and stretched by transform
|
||||
// alone, so the slide composites — animating its `top`/`height` would
|
||||
// force layout on every frame. Measured rather than computed from the
|
||||
// index: the buttons are not all the same height once a long filename
|
||||
// wraps.
|
||||
function placeIndicator() {
|
||||
const active = tree && tree.querySelector('.active');
|
||||
if (!active) return;
|
||||
tree.style.setProperty('--tab-x', active.offsetLeft + 'px');
|
||||
tree.style.setProperty('--tab-y', active.offsetTop + 'px');
|
||||
tree.style.setProperty('--tab-h', String(active.offsetHeight));
|
||||
}
|
||||
|
||||
function renderPackage(id) {
|
||||
const item = files.find((entry) => entry.id === id);
|
||||
if (!item) return;
|
||||
@@ -116,6 +131,7 @@ const packageFiles: PackageFile[] = [
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
placeIndicator();
|
||||
}
|
||||
|
||||
buttons.forEach(function (button) {
|
||||
@@ -124,6 +140,10 @@ const packageFiles: PackageFile[] = [
|
||||
});
|
||||
});
|
||||
|
||||
// Button heights change at the two breakpoints below (the caption is
|
||||
// dropped, the padding shrinks), which moves every offset under the bar.
|
||||
window.addEventListener('resize', placeIndicator);
|
||||
|
||||
renderPackage('skill');
|
||||
})();
|
||||
</script>
|
||||
@@ -139,11 +159,51 @@ const packageFiles: PackageFile[] = [
|
||||
}
|
||||
|
||||
.package-tree {
|
||||
/* Declared, not left to a `var()` fallback: audit-ui.mjs resolves every
|
||||
variable a page uses against the stylesheets it links, and a fallback
|
||||
does not count as a definition. The script below overrides these
|
||||
inline once it has measured the active button. */
|
||||
--tab-x: 0;
|
||||
--tab-y: 0;
|
||||
--tab-h: 0;
|
||||
|
||||
position: relative;
|
||||
padding: 22px 16px;
|
||||
border-right: 1px solid var(--muted);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Selection bar. One mark that slides between files, rather than a border
|
||||
that blinks off one button and on to another — the movement is what
|
||||
says "this is the same selection, now here". A 1px source box scaled by
|
||||
`--tab-h`, so both the travel and the resize are `transform`.
|
||||
|
||||
`--tab-y` / `--tab-h` are written by the script above. Until it runs the
|
||||
bar has zero height and is invisible, which is also what a JS-disabled
|
||||
reader sees; `aria-selected` on the buttons is what actually conveys
|
||||
the selection (.agents/rules/accessibility.md — motion is redundant
|
||||
reinforcement, never the only signal). */
|
||||
.package-tree::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: 1px;
|
||||
background: var(--gold);
|
||||
/* `--tab-x` because an absolutely positioned child resolves `left: 0`
|
||||
against the tree's padding box, which sits 16px (10px at the narrow
|
||||
breakpoint) left of where the buttons actually start. */
|
||||
transform: translate(var(--tab-x), var(--tab-y)) scaleY(var(--tab-h));
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.package-tree::after {
|
||||
transition: transform var(--dur-rise) var(--ease-out);
|
||||
}
|
||||
}
|
||||
|
||||
.package-tree > p,
|
||||
.package-preview > span {
|
||||
margin: 0 0 14px;
|
||||
@@ -167,16 +227,24 @@ const packageFiles: PackageFile[] = [
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* The 2px transparent border-left stays: it reserves the gutter the
|
||||
selection bar slides through, so the label never shifts when a file is
|
||||
selected. */
|
||||
.package-tree button:hover,
|
||||
.package-tree button:focus-visible,
|
||||
.package-tree button.active {
|
||||
border-left-color: var(--gold);
|
||||
background: var(--blue);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Gold belongs to the selection bar now, so focus gets its own mark
|
||||
instead of borrowing the border. Inset, so the tree's edge does not
|
||||
clip it. */
|
||||
.package-tree button:focus-visible {
|
||||
outline: 2px solid var(--gold);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.package-tree button:hover {
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
{
|
||||
"id": "agents",
|
||||
"eyebrow": { "en": "Subagent workflow", "pt": "Subagent workflow" },
|
||||
"title": { "en": "One branch<br>per <em>hand.</em>", "pt": "One branch<br>per <em>hand.</em>" },
|
||||
"eyebrow": {
|
||||
"en": "Subagent workflow",
|
||||
"pt": "Subagent workflow"
|
||||
},
|
||||
"title": {
|
||||
"en": "One branch<br>per <em>hand.</em>",
|
||||
"pt": "One branch<br>per <em>hand.</em>"
|
||||
},
|
||||
"lede": {
|
||||
"en": "Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent.",
|
||||
"pt": "Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent."
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
"label": { "en": "FRAME", "pt": "FRAME" },
|
||||
"title": { "en": "Orchestrator", "pt": "Orchestrator" },
|
||||
"label": {
|
||||
"en": "FRAME",
|
||||
"pt": "FRAME"
|
||||
},
|
||||
"title": {
|
||||
"en": "Orchestrator",
|
||||
"pt": "Orchestrator"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Owns scope, task graph, boundaries, and integration.",
|
||||
"pt": "Owns scope, task graph, boundaries, and integration."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "HAND OFF", "pt": "HAND OFF" },
|
||||
"title": { "en": "Worker", "pt": "Worker" },
|
||||
"label": {
|
||||
"en": "HAND OFF",
|
||||
"pt": "HAND OFF"
|
||||
},
|
||||
"title": {
|
||||
"en": "Worker",
|
||||
"pt": "Worker"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Owns one coherent slice and one worktree.",
|
||||
"pt": "Owns one coherent slice and one worktree."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "PROVE", "pt": "PROVE" },
|
||||
"title": { "en": "Verifier", "pt": "Verifier" },
|
||||
"label": {
|
||||
"en": "PROVE",
|
||||
"pt": "PROVE"
|
||||
},
|
||||
"title": {
|
||||
"en": "Verifier",
|
||||
"pt": "Verifier"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Re-runs gates and reports remaining gaps.",
|
||||
"pt": "Re-runs gates and reports remaining gaps."
|
||||
@@ -34,37 +58,58 @@
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"eyebrow": { "en": "The tree", "pt": "The tree" },
|
||||
"title": { "en": "Split at<br>the <em>seam.</em>", "pt": "Split at<br>the <em>seam.</em>" },
|
||||
"panelLabel": { "en": "MAIN / ORCHESTRATOR", "pt": "MAIN / ORCHESTRATOR" },
|
||||
"eyebrow": {
|
||||
"en": "The tree",
|
||||
"pt": "The tree"
|
||||
},
|
||||
"title": {
|
||||
"en": "Split at<br>the <em>seam.</em>",
|
||||
"pt": "Split at<br>the <em>seam.</em>"
|
||||
},
|
||||
"panelLabel": {
|
||||
"en": "MAIN / ORCHESTRATOR",
|
||||
"pt": "MAIN / ORCHESTRATOR"
|
||||
},
|
||||
"panelCode": {
|
||||
"en": "├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence",
|
||||
"pt": "├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence"
|
||||
"en": "├── agent/ui → components + visual states\n├── agent/tests → acceptance + regressions\n└── agent/docs → guide + examples\n\nmerge after each leaf returns a diff and evidence",
|
||||
"pt": "├── agent/ui → components + visual states\n├── agent/tests → acceptance + regressions\n└── agent/docs → guide + examples\n\nmerge after each leaf returns a diff and evidence"
|
||||
}
|
||||
},
|
||||
{
|
||||
"eyebrow": { "en": "Handoff", "pt": "Handoff" },
|
||||
"eyebrow": {
|
||||
"en": "Handoff",
|
||||
"pt": "Handoff"
|
||||
},
|
||||
"title": {
|
||||
"en": "Context that<br>can <em>travel.</em>",
|
||||
"pt": "Context that<br>can <em>travel.</em>"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"label": { "en": "Brief", "pt": "Brief" },
|
||||
"label": {
|
||||
"en": "Brief",
|
||||
"pt": "Brief"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Goal, owned files, dependencies, non-goals, acceptance.",
|
||||
"pt": "Goal, owned files, dependencies, non-goals, acceptance."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "Isolation", "pt": "Isolation" },
|
||||
"label": {
|
||||
"en": "Isolation",
|
||||
"pt": "Isolation"
|
||||
},
|
||||
"copy": {
|
||||
"en": "One branch and worktree per independent change.",
|
||||
"pt": "One branch and worktree per independent change."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "Evidence", "pt": "Evidence" },
|
||||
"label": {
|
||||
"en": "Evidence",
|
||||
"pt": "Evidence"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Commands, result, changed files, screenshots, gaps.",
|
||||
"pt": "Commands, result, changed files, screenshots, gaps."
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
{
|
||||
"id": "models",
|
||||
"eyebrow": { "en": "Model routing", "pt": "Model routing" },
|
||||
"title": { "en": "Choose the<br><em>engine.</em>", "pt": "Choose the<br><em>engine.</em>" },
|
||||
"eyebrow": {
|
||||
"en": "Model routing",
|
||||
"pt": "Model routing"
|
||||
},
|
||||
"title": {
|
||||
"en": "Choose the<br><em>engine.</em>",
|
||||
"pt": "Choose the<br><em>engine.</em>"
|
||||
},
|
||||
"lede": {
|
||||
"en": "A model has a capability ceiling. Effort controls how much room it gets to reason. Route by uncertainty and verification cost.",
|
||||
"pt": "A model has a capability ceiling. Effort controls how much room it gets to reason. Route by uncertainty and verification cost."
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
"label": { "en": "LOW", "pt": "LOW" },
|
||||
"title": { "en": "Bounded rhythm", "pt": "Bounded rhythm" },
|
||||
"label": {
|
||||
"en": "LOW",
|
||||
"pt": "LOW"
|
||||
},
|
||||
"title": {
|
||||
"en": "Bounded rhythm",
|
||||
"pt": "Bounded rhythm"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Lookup, small edits, formatting, and transformations with clear checks.",
|
||||
"pt": "Lookup, small edits, formatting, and transformations with clear checks."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "MEDIUM", "pt": "MEDIUM" },
|
||||
"title": { "en": "Default work", "pt": "Default work" },
|
||||
"label": {
|
||||
"en": "MEDIUM",
|
||||
"pt": "MEDIUM"
|
||||
},
|
||||
"title": {
|
||||
"en": "Default work",
|
||||
"pt": "Default work"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Normal implementation where the contract is clear but context matters.",
|
||||
"pt": "Normal implementation where the contract is clear but context matters."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "HIGH", "pt": "HIGH" },
|
||||
"title": { "en": "Ambiguity", "pt": "Ambiguity" },
|
||||
"label": {
|
||||
"en": "HIGH",
|
||||
"pt": "HIGH"
|
||||
},
|
||||
"title": {
|
||||
"en": "Ambiguity",
|
||||
"pt": "Ambiguity"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Planning, architecture, security judgment, and hard failures.",
|
||||
"pt": "Planning, architecture, security judgment, and hard failures."
|
||||
@@ -34,37 +58,58 @@
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"eyebrow": { "en": "Two knobs", "pt": "Two knobs" },
|
||||
"title": { "en": "Capability<br>× effort", "pt": "Capability<br>× effort" },
|
||||
"panelLabel": { "en": "ROUTING RULE", "pt": "ROUTING RULE" },
|
||||
"eyebrow": {
|
||||
"en": "Two knobs",
|
||||
"pt": "Two knobs"
|
||||
},
|
||||
"title": {
|
||||
"en": "Capability<br>× effort",
|
||||
"pt": "Capability<br>× effort"
|
||||
},
|
||||
"panelLabel": {
|
||||
"en": "ROUTING RULE",
|
||||
"pt": "ROUTING RULE"
|
||||
},
|
||||
"panelCode": {
|
||||
"en": "strong model + high effort → frame ambiguity · light model + low effort → bounded execution · raise one knob at a time → compare evidence",
|
||||
"pt": "strong model + high effort → frame ambiguity · light model + low effort → bounded execution · raise one knob at a time → compare evidence"
|
||||
"en": "strong model + high effort → frame ambiguity\nlight model + low effort → bounded execution\nraise one knob at a time → compare evidence",
|
||||
"pt": "strong model + high effort → frame ambiguity\nlight model + low effort → bounded execution\nraise one knob at a time → compare evidence"
|
||||
}
|
||||
},
|
||||
{
|
||||
"eyebrow": { "en": "Sequence", "pt": "Sequence" },
|
||||
"eyebrow": {
|
||||
"en": "Sequence",
|
||||
"pt": "Sequence"
|
||||
},
|
||||
"title": {
|
||||
"en": "Spend judgment<br>where it <em>compounds.</em>",
|
||||
"pt": "Spend judgment<br>where it <em>compounds.</em>"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"label": { "en": "Plan", "pt": "Plan" },
|
||||
"label": {
|
||||
"en": "Plan",
|
||||
"pt": "Plan"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Strong model: scope, risks, acceptance, and worktree split.",
|
||||
"pt": "Strong model: scope, risks, acceptance, and worktree split."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "Build", "pt": "Build" },
|
||||
"label": {
|
||||
"en": "Build",
|
||||
"pt": "Build"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Focused worker: smallest context and lightest model that can pass.",
|
||||
"pt": "Focused worker: smallest context and lightest model that can pass."
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": { "en": "Review", "pt": "Review" },
|
||||
"label": {
|
||||
"en": "Review",
|
||||
"pt": "Review"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Independent pass when missed issues cost more than the call.",
|
||||
"pt": "Independent pass when missed issues cost more than the call."
|
||||
|
||||
@@ -54,6 +54,62 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"eyebrow": { "en": "Check yourself", "pt": "Teste-se" },
|
||||
"title": {
|
||||
"en": "Recall it before<br>you <em>ship it.</em>",
|
||||
"pt": "Recupere antes<br>de <em>enviar.</em>"
|
||||
},
|
||||
"copy": {
|
||||
"en": "Answer from memory first — the reveal is the feedback.",
|
||||
"pt": "Responda de memória primeiro — a revelação é o feedback."
|
||||
},
|
||||
"recall": [
|
||||
{
|
||||
"question": {
|
||||
"en": "The skill never loads. What is the first suspect?",
|
||||
"pt": "A skill nunca carrega. Qual é o primeiro suspeito?"
|
||||
},
|
||||
"answer": {
|
||||
"en": "The trigger. A precise description says when to load the skill — and when to leave it out. A vague one never fires.",
|
||||
"pt": "O gatilho. Uma descrição precisa diz quando carregar a skill — e quando deixá-la de fora. Uma vaga nunca dispara."
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"en": "Where do the workflow, the facts, and the repeated mechanics each go?",
|
||||
"pt": "Onde vão o workflow, os fatos e as mecânicas repetidas?"
|
||||
},
|
||||
"answer": {
|
||||
"en": "The workflow stays in SKILL.md, conditional facts move to references/, and deterministic repeated mechanics become scripts/.",
|
||||
"pt": "O workflow fica no SKILL.md, fatos condicionais vão para references/, e mecânicas determinísticas repetidas viram scripts/."
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"en": "What proves a skill works?",
|
||||
"pt": "O que prova que uma skill funciona?"
|
||||
},
|
||||
"answer": {
|
||||
"en": "Behavior, not headings: realistic prompts, edge cases, and safety checks with observable evidence — the same bar the review desk applies.",
|
||||
"pt": "Comportamento, não títulos: prompts realistas, casos extremos e checagens de segurança com evidência observável — a mesma régua da review desk."
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"label": { "en": "Format specification ↗", "pt": "Especificação do formato ↗" },
|
||||
"href": "https://agentskills.io/specification"
|
||||
},
|
||||
{
|
||||
"label": {
|
||||
"en": "Try it on the Tiny Tasks lab →",
|
||||
"pt": "Experimente no lab Tiny Tasks →"
|
||||
},
|
||||
"href": "hands-on/starter/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -107,6 +107,12 @@ const skillInstallPrompts = defineCollection({
|
||||
// rule panels on /models/, /agents/, and /skills/ (the routing rule, the
|
||||
// MAIN / ORCHESTRATOR tree, and the package-anatomy hint).
|
||||
//
|
||||
// `sections[].recall` / `links` carry the retrieval-practice section on
|
||||
// /skills/ ("check yourself"): `recall` holds question/answer pairs that
|
||||
// render as native `<details>` (recall needs no JavaScript), and `links`
|
||||
// holds the citation + practice-loop row — `href` is either an absolute
|
||||
// external URL or a base-relative site path without the leading slash.
|
||||
//
|
||||
// `threadLabel` / `threadText` and `footer` carry the landing-page thread
|
||||
// strip and footer line.
|
||||
const chapters = defineCollection({
|
||||
@@ -147,6 +153,22 @@ const chapters = defineCollection({
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
recall: z
|
||||
.array(
|
||||
z.object({
|
||||
question: localized,
|
||||
answer: localized,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
links: z
|
||||
.array(
|
||||
z.object({
|
||||
label: localized,
|
||||
href: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"navPipeline": "Pipeline",
|
||||
"navSkills": "Skills",
|
||||
"navExamples": "Examples",
|
||||
"navRecall": "Recall",
|
||||
"stageContext": "CONTEXT",
|
||||
"stageReview": "REVIEW",
|
||||
"heroEyebrow": "A real repository case study",
|
||||
@@ -45,6 +46,17 @@
|
||||
"copyTitle": "Ask your agent to map the enforcement stack.",
|
||||
"copyText": "Use this in the interview repository or adapt the path names to another project.",
|
||||
"copyButton": "COPY PROMPT",
|
||||
"recallLabel": "Retrieval practice",
|
||||
"recallMeta": "answer before you open",
|
||||
"recallEyebrow": "Desirable difficulty",
|
||||
"recallTitle": "Close the <br />page. <em>Recall.</em>",
|
||||
"recallText": "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.",
|
||||
"recallQ1": "Where does a rule belong: context, skill, CLI check, or hook?",
|
||||
"recallA1": "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.",
|
||||
"recallQ2": "Why does a skill need a trigger, not just a workflow?",
|
||||
"recallA2": "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.",
|
||||
"recallQ3": "What separates a repository rule from a prompt?",
|
||||
"recallA3": "The prompt is advice for one run. The rule is reusable context plus an executable boundary — still present when the conversation is gone.",
|
||||
"deeperLabel": "GO DEEPER",
|
||||
"deeperTitle": "Read the implementation, not just this summary.",
|
||||
"deepContext": "Repository context",
|
||||
@@ -66,6 +78,7 @@
|
||||
"navPipeline": "Pipeline",
|
||||
"navSkills": "Skills",
|
||||
"navExamples": "Exemplos",
|
||||
"navRecall": "Recall",
|
||||
"stageContext": "CONTEXTO",
|
||||
"stageReview": "REVISÃO",
|
||||
"heroEyebrow": "Um estudo de caso de repositório real",
|
||||
@@ -107,6 +120,17 @@
|
||||
"copyTitle": "Peça ao agente para mapear o enforcement.",
|
||||
"copyText": "Use isto no repositório interview ou adapte os caminhos para outro projeto.",
|
||||
"copyButton": "COPIAR PROMPT",
|
||||
"recallLabel": "Prática de recuperação",
|
||||
"recallMeta": "responda antes de abrir",
|
||||
"recallEyebrow": "Dificuldade desejável",
|
||||
"recallTitle": "Feche o <br />guia. <em>Recupere.</em>",
|
||||
"recallText": "Prática de recuperação — lembrar a resposta da memória antes de reler — é o que constrói retenção de longo prazo. Responda cada pergunta de memória primeiro, depois abra para conferir.",
|
||||
"recallQ1": "Onde uma regra mora: contexto, skill, check de CLI ou hook?",
|
||||
"recallA1": "Orientação que o agente precisa descobrir vai no AGENTS.md ou numa skill. Política determinística vira comando de CLI, checks baratos rodam no commit, e julgamento vai para a revisão.",
|
||||
"recallQ2": "Por que uma skill precisa de um gatilho, não só de um workflow?",
|
||||
"recallA2": "O gatilho diz quando carregá-la. Sem ele a skill nunca dispara — ou carrega sempre — e uma skill que sempre carrega é só um prompt mais lento.",
|
||||
"recallQ3": "O que separa uma regra de repositório de um prompt?",
|
||||
"recallA3": "O prompt orienta uma execução. A regra é contexto reutilizável mais uma fronteira executável — ainda presente quando a conversa acaba.",
|
||||
"deeperLabel": "APROFUNDE",
|
||||
"deeperTitle": "Leia a implementação, não apenas este resumo.",
|
||||
"deepContext": "Contexto do repositório",
|
||||
|
||||
@@ -14,6 +14,9 @@ import BaseLayout from './BaseLayout.astro';
|
||||
import TopBar from '../components/blocks/TopBar.astro';
|
||||
import SiteFooter from '../components/blocks/SiteFooter.astro';
|
||||
import chaptersStylesheet from '../../legacy/styles/chapters.css?url';
|
||||
// Shared motion layer (scroll reveals, hero parallax, load entrances).
|
||||
// Opt-in classes; see src/styles/motion.css for the gating contract.
|
||||
import motionStylesheet from '../styles/motion.css?url';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -26,6 +29,7 @@ const { title, description, lang = 'en' } = Astro.props;
|
||||
|
||||
<BaseLayout title={title} description={description} lang={lang}>
|
||||
<link slot="styles" rel="stylesheet" href={chaptersStylesheet} />
|
||||
<link slot="styles" rel="stylesheet" href={motionStylesheet} />
|
||||
<TopBar id="top">
|
||||
<slot name="top-previous" slot="previous" />
|
||||
<slot name="top-center" slot="center" />
|
||||
|
||||
+59
-20
@@ -3,9 +3,12 @@
|
||||
// Identical URL (/agents/), zero client JS, copy lives in
|
||||
// src/content/chapters/agents.json.
|
||||
|
||||
import { getEntry } from 'astro:content';
|
||||
import { getCollection, getEntry } from 'astro:content';
|
||||
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||
import DrawRule from '../components/blocks/DrawRule.astro';
|
||||
import StepFlow from '../components/blocks/StepFlow.astro';
|
||||
import WorktreeMap from '../components/blocks/WorktreeMap.astro';
|
||||
|
||||
// Required-field guard. The chapters schema marks section eyebrow /
|
||||
// panelLabel / panelCode / steps / copy as optional because the schema
|
||||
@@ -43,6 +46,21 @@ const treePanelLabel = requireField(treeSection.panelLabel, 'sections[0].panelLa
|
||||
const treePanelCode = requireField(treeSection.panelCode, 'sections[0].panelCode');
|
||||
const handoffEyebrow = requireField(handoffSection.eyebrow, 'sections[1].eyebrow');
|
||||
const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
|
||||
|
||||
// The worktree map draws the same three leaves the panel above it lists, so
|
||||
// it takes its copy from the `trees` collection the full guide already uses
|
||||
// rather than introducing a second set of strings for the same checkouts
|
||||
// (.agents/rules/content-i18n.md). English only: this page has no language
|
||||
// toggle, and emitting the hidden `pt` spans would put untoggleable
|
||||
// Portuguese into the rendered-text snapshot.
|
||||
const trees = Object.fromEntries((await getCollection('trees')).map(({ data }) => [data.id, data]));
|
||||
const treeBranches = ['ui', 'tests', 'docs'].map((id) => ({
|
||||
id,
|
||||
label: trees[id].owner.en,
|
||||
strong: `agent/${id}`,
|
||||
small: `● ${trees[id].status}`,
|
||||
tone: id,
|
||||
}));
|
||||
---
|
||||
|
||||
<ChapterLayout title="AI For Dummies — Agents and trees" description={lede.replace(/<[^>]+>/g, '')}>
|
||||
@@ -55,18 +73,32 @@ const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
|
||||
<p>{lede}</p>
|
||||
</ChapterHero>
|
||||
|
||||
<section class="pipeline">
|
||||
<div>
|
||||
<section class="pipeline reveal">
|
||||
<div class="depth-slow">
|
||||
<p class="eyebrow">{treeEyebrow.en}</p>
|
||||
<h2 set:html={treeSection.title.en} />
|
||||
<DrawRule shape="bracket" height={110} />
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel depth-fast">
|
||||
<strong>{treePanelLabel.en}</strong>
|
||||
<code>{treePanelCode.en}</code>
|
||||
</div>
|
||||
{
|
||||
/* Same three leaves as the panel above, as topology rather than as a
|
||||
listing. Static: this page ships no JS, so the nodes are not
|
||||
selectable and do not claim to be. */
|
||||
}
|
||||
<div class="tree-figure">
|
||||
<WorktreeMap
|
||||
branches={treeBranches}
|
||||
interactive={false}
|
||||
rootLabel={trees.main.owner.en}
|
||||
rootSmall={`● ${trees.main.status}`}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<section class="grid reveal">
|
||||
{
|
||||
cards.map((card) => (
|
||||
<article class="card">
|
||||
@@ -78,24 +110,16 @@ const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="practice">
|
||||
<div>
|
||||
<section class="practice reveal">
|
||||
<div class="depth-slow">
|
||||
<p class="eyebrow">{handoffEyebrow.en}</p>
|
||||
<h2 set:html={handoffSection.title.en} />
|
||||
<DrawRule height={110} />
|
||||
</div>
|
||||
<div class="steps">
|
||||
{
|
||||
handoffSteps.map((step, index) => (
|
||||
<article>
|
||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||
<div>
|
||||
<strong>{step.label.en}</strong>
|
||||
<span>{step.copy.en}</span>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<StepFlow
|
||||
class="depth-fast"
|
||||
steps={handoffSteps.map((step) => ({ label: step.label.en, copy: step.copy.en }))}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
|
||||
@@ -104,3 +128,18 @@ const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
|
||||
<a href={`${base}hands-on/rules/`}>Try the rules lab →</a>
|
||||
</nav>
|
||||
</ChapterLayout>
|
||||
|
||||
<style>
|
||||
/* The map illustrates both columns, so it spans them. `min-width: 0` on
|
||||
the panel because a grid item defaults to `min-content` width and the
|
||||
panel now holds a preformatted tree that must be allowed to scroll
|
||||
rather than widen the page. */
|
||||
.pipeline > .panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pipeline > .tree-figure {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,7 +48,7 @@ const cards = data.cards ?? [];
|
||||
>
|
||||
</ChapterHero>
|
||||
|
||||
<GridGroup label="Guide chapters" columns={3}>
|
||||
<GridGroup label="Guide chapters" columns={3} class="reveal">
|
||||
{
|
||||
cards.map((card) => (
|
||||
<RouteCard
|
||||
@@ -62,7 +62,7 @@ const cards = data.cards ?? [];
|
||||
}
|
||||
</GridGroup>
|
||||
|
||||
<section class="landing-note">
|
||||
<section class="landing-note reveal">
|
||||
<span>{data.threadLabel?.[lang]}</span>
|
||||
<strong>{data.threadText?.[lang]}</strong>
|
||||
</section>
|
||||
|
||||
+23
-19
@@ -7,6 +7,8 @@
|
||||
import { getEntry } from 'astro:content';
|
||||
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||
import DrawRule from '../components/blocks/DrawRule.astro';
|
||||
import StepFlow from '../components/blocks/StepFlow.astro';
|
||||
|
||||
// Required-field guard. The chapters schema marks section eyebrow /
|
||||
// panelLabel / panelCode / steps / copy as optional because the schema
|
||||
@@ -55,7 +57,7 @@ const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
|
||||
<p>{lede}</p>
|
||||
</ChapterHero>
|
||||
|
||||
<section class="grid">
|
||||
<section class="grid reveal">
|
||||
{
|
||||
cards.map((card) => (
|
||||
<article class="card">
|
||||
@@ -67,35 +69,28 @@ const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="model">
|
||||
<div>
|
||||
<section class="model reveal">
|
||||
<div class="depth-slow">
|
||||
<p class="eyebrow">{ruleEyebrow.en}</p>
|
||||
<h2 set:html={ruleSection.title.en} />
|
||||
<DrawRule shape="bracket" height={110} />
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel depth-fast">
|
||||
<strong>{rulePanelLabel.en}</strong>
|
||||
<code>{rulePanelCode.en}</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="practice">
|
||||
<div>
|
||||
<section class="practice reveal">
|
||||
<div class="depth-slow">
|
||||
<p class="eyebrow">{sequenceEyebrow.en}</p>
|
||||
<h2 set:html={sequenceSection.title.en} />
|
||||
<DrawRule height={110} />
|
||||
</div>
|
||||
<div class="steps">
|
||||
{
|
||||
sequenceSteps.map((step, index) => (
|
||||
<article>
|
||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||
<div>
|
||||
<strong>{step.label.en}</strong>
|
||||
<span>{step.copy.en}</span>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<StepFlow
|
||||
class="depth-fast"
|
||||
steps={sequenceSteps.map((step) => ({ label: step.label.en, copy: step.copy.en }))}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
|
||||
@@ -103,3 +98,12 @@ const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
|
||||
<a href={`${base}rules/`}>Rules case study →</a>
|
||||
</nav>
|
||||
</ChapterLayout>
|
||||
|
||||
<style>
|
||||
/* A grid item defaults to `min-content` width, and the panel now holds a
|
||||
preformatted routing table; without this it widens the page instead of
|
||||
scrolling. */
|
||||
.model > .panel {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,11 +13,15 @@
|
||||
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import RulesInteractive from '../components/islands/RulesInteractive.astro';
|
||||
// Shared motion layer — scroll reveals, hero parallax, load entrances.
|
||||
// Opt-in classes; see src/styles/motion.css for the gating contract.
|
||||
import motionStylesheet from '../styles/motion.css?url';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Rules That Survive the Prompt — AI For Dummies"
|
||||
description="A concise case study of agent skills, CLI ratchets, Husky hooks, and PR review in netcracker/interview."
|
||||
>
|
||||
<link slot="styles" rel="stylesheet" href={motionStylesheet} />
|
||||
<RulesInteractive />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -164,6 +164,7 @@ const serializedCatalog = JSON.stringify(catalog).replace(/</g, '\\u003c');
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: start;
|
||||
max-height: calc(100dvh - 48px);
|
||||
overflow: hidden;
|
||||
@@ -171,6 +172,12 @@ const serializedCatalog = JSON.stringify(catalog).replace(/</g, '\\u003c');
|
||||
|
||||
#skill-list {
|
||||
min-height: 0;
|
||||
/* Rows nudge 3px right on hover/focus (see below); on a scroll container
|
||||
that widens the scrollable area, surfacing a horizontal scrollbar that a
|
||||
focus scroll-into-view can leave stuck scrolled. The list only ever
|
||||
scrolls vertically, so clip the axis outright — `clip`, unlike `hidden`,
|
||||
cannot be scrolled programmatically. */
|
||||
overflow-x: clip;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-color: var(--blue) var(--paper);
|
||||
|
||||
+214
-16
@@ -8,7 +8,17 @@ import { getEntry } from 'astro:content';
|
||||
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||
import SkillPackageExplorer from '../components/islands/SkillPackageExplorer.astro';
|
||||
import skillsStylesheet from '../../legacy/styles/skills.css?url';
|
||||
import DrawRule from '../components/blocks/DrawRule.astro';
|
||||
import StepFlow from '../components/blocks/StepFlow.astro';
|
||||
// A plain side-effect import, not the `?url` + `<link slot="styles">` pattern
|
||||
// the layout uses for its legacy sheets. `?url` inside a *page* resolves to
|
||||
// that page's own CSS chunk whatever file it names, so the link pointed at
|
||||
// the scoped bundle while these rules were emitted to a second asset nothing
|
||||
// loaded. The visible symptom was the JS-injected package preview rendering
|
||||
// unstyled, its <pre> overflowing the page instead of scrolling. Renamed off
|
||||
// `skills.css` at the same time so the two assets are told apart in a build
|
||||
// listing.
|
||||
import '../../legacy/styles/skills-chapter.css';
|
||||
|
||||
// Required-field guard. The chapters schema marks section eyebrow /
|
||||
// panelLabel / panelCode / steps / copy as optional because the schema
|
||||
@@ -42,10 +52,23 @@ const anatomyEyebrow = requireField(anatomySection.eyebrow, 'sections[0].eyebrow
|
||||
const anatomyCopy = requireField(anatomySection.copy, 'sections[0].copy');
|
||||
const createEyebrow = requireField(createSection.eyebrow, 'sections[1].eyebrow');
|
||||
const createSteps = requireField(createSection.steps, 'sections[1].steps');
|
||||
// Retrieval-practice section ("check yourself") — teach-skill structure:
|
||||
// recall from memory, then open the answer for feedback. The citation and
|
||||
// practice-loop links ground the section in the spec and the hands-on lab.
|
||||
const recallSection = sections[2];
|
||||
if (!recallSection) {
|
||||
throw new Error('skills chapter: missing sections[2]');
|
||||
}
|
||||
const recallEyebrow = requireField(recallSection.eyebrow, 'sections[2].eyebrow');
|
||||
const recallCopy = requireField(recallSection.copy, 'sections[2].copy');
|
||||
const recallItems = requireField(recallSection.recall, 'sections[2].recall');
|
||||
const recallLinks = requireField(recallSection.links, 'sections[2].links');
|
||||
// `href` is either an absolute external URL or a base-relative site path.
|
||||
const linkHref = (href: string) =>
|
||||
/^https?:/i.test(href) ? href : `${import.meta.env.BASE_URL}${href}`;
|
||||
---
|
||||
|
||||
<ChapterLayout title="AI For Dummies — Skills" description={lede.replace(/<[^>]+>/g, '')}>
|
||||
<link slot="styles" rel="stylesheet" href={skillsStylesheet} />
|
||||
<a slot="top-previous" href={`${base}summary/`}>← ROUTE MAP</a>
|
||||
<span slot="top-center">03 / SKILLS</span>
|
||||
<a slot="top-next" href={`${base}skills-review/`}>review desk ↗</a>
|
||||
@@ -55,30 +78,72 @@ const createSteps = requireField(createSection.steps, 'sections[1].steps');
|
||||
<p set:html={lede} />
|
||||
</ChapterHero>
|
||||
|
||||
<section class="pipeline package-anatomy">
|
||||
<div>
|
||||
{
|
||||
/* `depth-slow` / `depth-fast` are the shared motion layer's two parallax
|
||||
rates (src/styles/motion.css): the heading column drifts further than
|
||||
the panel beside it, so the two read at different distances. The
|
||||
`DrawRule` bracket ties the heading to the package explorer — it draws
|
||||
itself as the section enters. Both are opt-in classes on a section
|
||||
that already carries `reveal`; nothing here is required for the page
|
||||
to make sense, and neither runs without scroll-driven-animation
|
||||
support or with reduced motion. */
|
||||
}
|
||||
<section class="pipeline package-anatomy reveal">
|
||||
<div class="depth-slow">
|
||||
<p class="eyebrow">{anatomyEyebrow.en}</p>
|
||||
<h2 set:html={anatomySection.title.en} />
|
||||
<p class="package-hint">{anatomyCopy.en}</p>
|
||||
<DrawRule shape="bracket" height={150} />
|
||||
</div>
|
||||
<div class="depth-fast">
|
||||
<SkillPackageExplorer />
|
||||
</div>
|
||||
<SkillPackageExplorer />
|
||||
</section>
|
||||
|
||||
<section class="practice">
|
||||
<div>
|
||||
<section class="practice reveal">
|
||||
<div class="depth-slow">
|
||||
<p class="eyebrow">{createEyebrow.en}</p>
|
||||
<h2 set:html={createSection.title.en} />
|
||||
<DrawRule height={110} />
|
||||
</div>
|
||||
<div class="steps">
|
||||
<StepFlow
|
||||
class="depth-fast"
|
||||
steps={createSteps.map((step) => ({ label: step.label.en, copy: step.copy.en }))}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="practice recall reveal" id="recall">
|
||||
<div class="depth-slow">
|
||||
<p class="eyebrow">{recallEyebrow.en}</p>
|
||||
<h2 set:html={recallSection.title.en} />
|
||||
<p class="recall-note">{recallCopy.en}</p>
|
||||
<div class="recall-links">
|
||||
{
|
||||
recallLinks.map((link) => {
|
||||
const external = /^https?:/i.test(link.href);
|
||||
return (
|
||||
<a
|
||||
href={linkHref(link.href)}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noreferrer' : undefined}
|
||||
>
|
||||
{link.label.en}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="recall-list depth-fast">
|
||||
{
|
||||
createSteps.map((step, index) => (
|
||||
<article>
|
||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||
<div>
|
||||
<strong>{step.label.en}</strong>
|
||||
<span>{step.copy.en}</span>
|
||||
</div>
|
||||
</article>
|
||||
recallItems.map((item) => (
|
||||
<details>
|
||||
<summary>
|
||||
<span>{item.question.en}</span>
|
||||
<b aria-hidden="true">+</b>
|
||||
</summary>
|
||||
<p>{item.answer.en}</p>
|
||||
</details>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@@ -91,3 +156,136 @@ const createSteps = requireField(createSection.steps, 'sections[1].steps');
|
||||
<a href={`${base}full-guide/#create-skill`}>Full guide: skill forge →</a>
|
||||
</nav>
|
||||
</ChapterLayout>
|
||||
|
||||
<style>
|
||||
/* Page-level styles only; reusable pieces belong in a component
|
||||
(.agents/rules/componentization.md). The recall section reuses the
|
||||
chapters.css `.practice` grid; these rules style the native <details>
|
||||
recall cards and the citation/practice link row it carries. */
|
||||
|
||||
/* The depth wrapper stands in as the grid item the workbench used to be.
|
||||
A grid item defaults to `min-width: auto`, which would let the code
|
||||
samples inside the workbench force the section wider than the viewport
|
||||
— the horizontal-scroll condition audit-ui.mjs fails the build on. */
|
||||
.package-anatomy > .depth-fast {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recall-note {
|
||||
max-width: 680px;
|
||||
margin: 18px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.recall-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
/* Same pill treatment as the chapter footer links (chapters.css
|
||||
`.links a`) so the citation row reads as part of the page's nav
|
||||
vocabulary, not a new component. */
|
||||
.recall-links a {
|
||||
padding: 10px 13px;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--ink);
|
||||
text-decoration: none;
|
||||
font: 700 var(--step-00) var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
transition:
|
||||
color var(--dur-press) var(--ease-out),
|
||||
background-color var(--dur-press) var(--ease-out),
|
||||
transform var(--dur-press) var(--ease-out);
|
||||
}
|
||||
|
||||
.recall-links a:hover {
|
||||
color: var(--paper);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.recall-links a:focus-visible {
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.recall-links a:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.recall-links a {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.recall-list {
|
||||
display: grid;
|
||||
gap: 1px; /* hairline separators, drawn by the parent background */
|
||||
background: var(--line);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.recall-list details {
|
||||
padding: 18px 20px;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.recall-list summary {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
cursor: pointer;
|
||||
/* Hide the UA triangle on both engines; the `+`/`×` marker below is
|
||||
the indicator, and it is decorative (`aria-hidden`). */
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.recall-list summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.recall-list summary span {
|
||||
font-size: var(--step-1);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.recall-list summary b {
|
||||
color: var(--red);
|
||||
font: 700 var(--step-16) var(--font-mono);
|
||||
transition: transform var(--dur-press) var(--ease-out);
|
||||
}
|
||||
|
||||
/* `+` becomes `×` — the only state change the marker needs. */
|
||||
.recall-list details[open] summary b {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.recall-list details p {
|
||||
max-width: 680px;
|
||||
margin: 12px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* purpose: make the revealed answer legible as a state change */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.recall-list details[open] p {
|
||||
animation: recall-answer-in var(--dur-rise) var(--ease-out) both;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes recall-answer-in {
|
||||
from {
|
||||
opacity: 0.01;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,7 @@ const introduction =
|
||||
<p>{introduction}</p>
|
||||
</ChapterHero>
|
||||
|
||||
<SectionGrid>
|
||||
<SectionGrid class="reveal">
|
||||
<article class="card">
|
||||
<b>01</b><h2>Models</h2><p>Capability and effort are separate knobs.</p><a
|
||||
href={`${base}models/`}>Open chapter →</a
|
||||
|
||||
@@ -37,3 +37,15 @@ a:hover {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 2560px) {
|
||||
body {
|
||||
zoom: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 3840px) {
|
||||
body {
|
||||
zoom: 1.75;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/* Motion layer — scroll reveals, hero parallax, section depth, line draw,
|
||||
load entrances. Loaded by ChapterLayout (every chapter page) and /rules/
|
||||
via a `?url` import; the classes are opt-in per element.
|
||||
|
||||
Everything here is progressive enhancement with two gates:
|
||||
|
||||
- Scrubbed motion (scroll reveal, parallax, depth, line draw) is nested in
|
||||
`@supports (animation-timeline: view())`, so browsers without scroll-
|
||||
driven animations render the fully static page — no pre-animated
|
||||
sections, no half-run states.
|
||||
- All motion is nested in `prefers-reduced-motion: no-preference`, so
|
||||
opted-out users get the static page by construction rather than by
|
||||
subtracting motion from them.
|
||||
|
||||
Scrubbed animations use `linear` easing: the scroll position is the
|
||||
timing function, and any curve here would double-ease. Time-based
|
||||
entrances use the house curve from tokens.css. `transform` and
|
||||
`opacity` only — both composite on the GPU and keep the 200ms INP
|
||||
budget intact (.agents/rules/animation.md).
|
||||
|
||||
Keyframes and reveals deliberately have no stagger: sections move as one
|
||||
unit, which is this site's stated point of view on cascades. */
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
/* Scroll reveal: a section rises into place as it enters the viewport.
|
||||
`both` holds the hidden start state until entry begins, and the range
|
||||
ends at 45% of entry, so a section is fully settled before its top
|
||||
reaches mid-screen — reading pace is never gated on the animation.
|
||||
Anchor jumps land past the range and render the final state. */
|
||||
@supports (animation-timeline: view()) {
|
||||
.reveal {
|
||||
animation: reveal-rise linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry 0% entry 45%;
|
||||
}
|
||||
|
||||
/* Parallax: hero pieces lag the scroll at two depths while the hero
|
||||
exits through the top of the viewport. The distances are small on
|
||||
purpose — this is depth, not spectacle — and each drift completes
|
||||
only once its element has left the screen. */
|
||||
.parallax-near {
|
||||
animation: parallax-near linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: exit 0% exit 100%;
|
||||
}
|
||||
|
||||
.parallax-far {
|
||||
animation: parallax-far linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: exit 0% exit 100%;
|
||||
}
|
||||
|
||||
/* Section depth: the two columns of a `.pipeline` / `.practice` grid
|
||||
drift at different rates for the whole time the section is on screen,
|
||||
so the heading reads at a different distance from the panel beside it.
|
||||
This is the hero's parallax extended to mid-page sections, and the
|
||||
range differs for a reason — the hero animates on `exit` because it
|
||||
starts already on screen, while a mid-page section is watched from
|
||||
first pixel to last, so the range is `entry 0% exit 100%`.
|
||||
|
||||
Not a stagger. Nothing waits its turn: both columns move continuously
|
||||
and simultaneously, only at different rates. The no-cascade point of
|
||||
view in the header still holds. */
|
||||
.depth-slow {
|
||||
animation: depth-slow linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry 0% exit 100%;
|
||||
}
|
||||
|
||||
.depth-fast {
|
||||
animation: depth-fast linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry 0% exit 100%;
|
||||
}
|
||||
|
||||
/* Line draw: a hairline paints itself along its own path as the section
|
||||
scrolls in. This is the native stand-in for the frame-by-frame runtime
|
||||
this site refuses to ship — a Lottie player is ~60KB gzipped driving
|
||||
`requestAnimationFrame` on the main thread, against a 200ms INP
|
||||
budget; this is 0KB, and the scroll position is the clock.
|
||||
|
||||
`pathLength="100"` on the path normalises its geometry to 100 units,
|
||||
so these numbers hold for any rendered size with no JS measurement
|
||||
step. `stroke-dasharray` and `stroke-dashoffset` must both equal that
|
||||
length, or the line starts part-drawn (offset too small) or never
|
||||
closes (offset too large).
|
||||
|
||||
`stroke-dashoffset` is the one property here that is not `transform`
|
||||
or `opacity`. It repaints the path; it does not force layout, and the
|
||||
subject is a 1px hairline, so the cost is a repaint of a few hundred
|
||||
pixels per frame. That is the deliberate exception to the
|
||||
transform/opacity rule in .agents/rules/animation.md — the only way
|
||||
to draw a line along a path is to change how it is stroked.
|
||||
|
||||
The range is `cover`, not `entry`. Every `view()` range keys to first
|
||||
visibility, and for a section on a tall page that is long before the
|
||||
section is read — an `entry`-based draw measured out at scrollY
|
||||
190-654 on /skills/, finishing while the reader was still on the hero.
|
||||
`cover 40%` to `cover 75%` is the band where the subject sits around
|
||||
the middle of the screen, which is where a reader actually is. */
|
||||
.draw-line path {
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
animation: line-draw linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: cover 40% cover 75%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Load entrance for above-the-fold heroes. Time-based, not scrubbed: one
|
||||
coordinated rise of the whole hero block in the house curve. */
|
||||
.rise-in {
|
||||
animation: load-rise var(--dur-rise) var(--ease-out) both;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reveal-rise {
|
||||
from {
|
||||
opacity: 0.01;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes parallax-near {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(14px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes parallax-far {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(30px);
|
||||
}
|
||||
}
|
||||
|
||||
/* The two depth rates. Same direction, different magnitude — that is what
|
||||
reads as distance; opposite directions read as the page coming apart.
|
||||
The differential is the whole effect, so it has to clear the perception
|
||||
threshold: an earlier 22/6 pair measured out at 3-10px of separation
|
||||
across the window where a section is actually read, which is to say no
|
||||
effect at all. 38/10 puts ~25px of separation in that window and still
|
||||
reads as drift rather than spectacle. */
|
||||
@keyframes depth-slow {
|
||||
from {
|
||||
transform: translateY(38px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-38px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes depth-fast {
|
||||
from {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes line-draw {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load-rise {
|
||||
from {
|
||||
opacity: 0.01;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Printing freezes animations at their current time; a not-yet-entered
|
||||
section would print invisible. Static for paper. */
|
||||
@media print {
|
||||
.reveal,
|
||||
.parallax-near,
|
||||
.parallax-far,
|
||||
.depth-slow,
|
||||
.depth-fast,
|
||||
.rise-in {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* A drawn line frozen mid-animation prints as a fragment. Undo the dash
|
||||
pattern rather than the animation, so the full rule prints. */
|
||||
.draw-line path {
|
||||
animation: none;
|
||||
stroke-dasharray: none;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,15 @@
|
||||
--guide-prompt-rule: #ffffff32;
|
||||
--guide-prompt-enhanced-copy: #e5e3ef;
|
||||
|
||||
/* Motion. The house curve is the strong ease-out already shipped inline
|
||||
across pages (base.css link transitions, the review desk, the rules
|
||||
island); naming it here lets new motion extend the same curve instead
|
||||
of re-typing near-misses. Durations follow the 150-250ms UI budget in
|
||||
.agents/rules/animation.md. */
|
||||
--ease-out: cubic-bezier(0.2, 0, 0, 1);
|
||||
--dur-press: 160ms;
|
||||
--dur-rise: 200ms;
|
||||
|
||||
/* Breakpoints */
|
||||
--bp-sm: 560px;
|
||||
--bp-md: 800px;
|
||||
|
||||
Reference in New Issue
Block a user