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
|
- **`legacy/styles/audit.css`** (was `full-guide/audit.css`) — responsive audit
|
||||||
overrides, imported by `full-guide.astro`.
|
overrides, imported by `full-guide.astro`.
|
||||||
- **`legacy/styles/chapters.css`** — imported by `ChapterLayout.astro`.
|
- **`legacy/styles/chapters.css`** — imported by `ChapterLayout.astro`.
|
||||||
- **`legacy/styles/skills.css`**, **`skills-review.css`**, **`change-lens.css`**
|
- **`legacy/styles/skills-chapter.css`**, **`skills-review.css`**,
|
||||||
— imported by their respective pages.
|
**`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
|
- **`legacy/skills-review/`** — `app.js` and the module graph under it
|
||||||
(`catalog.js`, `submitted-catalog.js`, `files.js`, `submitted-files.js`,
|
(`catalog.js`, `submitted-catalog.js`, `files.js`, `submitted-files.js`,
|
||||||
`vote.js`). `catalog.js` + `submitted-catalog.js` are the review desk's real
|
`vote.js`). `catalog.js` + `submitted-catalog.js` are the review desk's real
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ a bug.
|
|||||||
```bash
|
```bash
|
||||||
python3 - <<'PY'
|
python3 - <<'PY'
|
||||||
import re
|
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/skills-review.css','legacy/styles/change-lens.css',
|
||||||
'legacy/styles/audit.css','public/hands-on/starter/styles.css',
|
'legacy/styles/audit.css','public/hands-on/starter/styles.css',
|
||||||
'public/hands-on/rules/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.
|
- One thing moves at a time. No staggered card cascades.
|
||||||
- `will-change` only immediately before animating, removed after.
|
- `will-change` only immediately before animating, removed after.
|
||||||
- No animation library. This site's thesis is having no runtime dependencies.
|
- 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
|
## 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
|
the
|
||||||
seam.
|
seam.
|
||||||
MAIN / ORCHESTRATOR
|
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
|
FRAME
|
||||||
Orchestrator
|
Orchestrator
|
||||||
Owns scope, task graph, boundaries, and integration.
|
Owns scope, task graph, boundaries, and integration.
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ Two knobs
|
|||||||
Capability
|
Capability
|
||||||
× effort
|
× effort
|
||||||
ROUTING RULE
|
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
|
Sequence
|
||||||
Spend judgment
|
Spend judgment
|
||||||
where it
|
where it
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ field guide
|
|||||||
Pipeline
|
Pipeline
|
||||||
Skills
|
Skills
|
||||||
Examples
|
Examples
|
||||||
|
Recall
|
||||||
EN
|
EN
|
||||||
/
|
/
|
||||||
PT
|
PT
|
||||||
@@ -94,6 +95,22 @@ A second reader checks intent.
|
|||||||
├── languages
|
├── languages
|
||||||
└── instructions
|
└── instructions
|
||||||
Read review policy →
|
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
|
COPY / ADAPT
|
||||||
Ask your agent to map the enforcement stack.
|
Ask your agent to map the enforcement stack.
|
||||||
Use this in the interview repository or adapt the path names to another project.
|
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
|
04
|
||||||
Evaluate behavior
|
Evaluate behavior
|
||||||
Test realistic prompts, edge cases, safety, and evidence.
|
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 →
|
Agents & trees →
|
||||||
Rules case study →
|
Rules case study →
|
||||||
Review submitted skills →
|
Review submitted skills →
|
||||||
|
|||||||
@@ -3,4 +3,12 @@ import { defineConfig } from 'astro/config';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/ai-for-dummies',
|
base: '/ai-for-dummies',
|
||||||
trailingSlash: 'always',
|
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');
|
throw new Error('built rules page lost its accessible controls');
|
||||||
if (!html.rules.includes('data-rules-copy') || !html.rules.includes('"pt"'))
|
if (!html.rules.includes('data-rules-copy') || !html.rules.includes('"pt"'))
|
||||||
throw new Error('built rules page lost its Portuguese interaction data');
|
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 (
|
if (
|
||||||
!has(html.review, [
|
!has(html.review, [
|
||||||
@@ -233,6 +244,25 @@ if (
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
throw new Error('built skills page lost its package explorer');
|
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"']))
|
if (!has(html.starter, ['id="task-list"', 'id="task-count"']))
|
||||||
throw new Error('starter lab lost its task-list contract');
|
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
|
// The eyebrow reuses the existing `Eyebrow` primitive but defaults to the
|
||||||
// `red` tone, which matches the chapter surfaces (not the guide surface).
|
// `red` tone, which matches the chapter surfaces (not the guide surface).
|
||||||
// The h1 em treatment (Georgia italic, red) is the chapter-page signature.
|
// 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';
|
import Eyebrow from '../primitives/Eyebrow.astro';
|
||||||
|
|
||||||
@@ -20,10 +25,10 @@ interface Props {
|
|||||||
const { eyebrow, tone = 'red' } = Astro.props;
|
const { eyebrow, tone = 'red' } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero rise-in">
|
||||||
<Eyebrow label={eyebrow} tone={tone} />
|
<Eyebrow label={eyebrow} tone={tone} />
|
||||||
<h1><slot name="title" /></h1>
|
<h1 class="parallax-near"><slot name="title" /></h1>
|
||||||
<div class="intro"><slot /></div>
|
<div class="intro parallax-far"><slot /></div>
|
||||||
<slot name="foot" />
|
<slot name="foot" />
|
||||||
</section>
|
</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;
|
label: string;
|
||||||
/** Number of columns at the widest breakpoint. */
|
/** Number of columns at the widest breakpoint. */
|
||||||
columns?: number;
|
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}`}>
|
<div class="grid" style={`--columns: ${columns}`}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -29,6 +31,12 @@ const { label, columns = 3 } = Astro.props;
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.grid {
|
.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;
|
display: grid;
|
||||||
grid-template-columns: repeat(var(--columns), 1fr);
|
grid-template-columns: repeat(var(--columns), 1fr);
|
||||||
gap: 1px; /* hairline separators, drawn by the parent background */
|
gap: 1px; /* hairline separators, drawn by the parent background */
|
||||||
|
|||||||
@@ -14,17 +14,25 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
/** Number of columns at the widest breakpoint. */
|
/** Number of columns at the widest breakpoint. */
|
||||||
columns?: number;
|
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 />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.grid {
|
.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;
|
display: grid;
|
||||||
grid-template-columns: repeat(var(--columns), 1fr);
|
grid-template-columns: repeat(var(--columns), 1fr);
|
||||||
gap: 1px; /* hairline separators, drawn by the parent background */
|
gap: 1px; /* hairline separators, drawn by the parent background */
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ const { navLabel = 'Chapter navigation' } = Astro.props;
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.footer {
|
.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);
|
color: var(--muted);
|
||||||
/* UNRESOLVED: legacy 13px fixed from chapters.css. No token matches.
|
/* UNRESOLVED: legacy 13px fixed from chapters.css. No token matches.
|
||||||
Reported in task 09 report. */
|
Reported in task 09 report. */
|
||||||
@@ -49,6 +54,13 @@ const { navLabel = 'Chapter navigation' } = Astro.props;
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font: var(--step-0) var(--font-mono);
|
font: var(--step-0) var(--font-mono);
|
||||||
text-transform: uppercase;
|
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) {
|
.links :global(a:hover) {
|
||||||
@@ -60,4 +72,27 @@ const { navLabel = 'Chapter navigation' } = Astro.props;
|
|||||||
outline: 3px solid var(--red);
|
outline: 3px solid var(--red);
|
||||||
outline-offset: 2px;
|
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>
|
</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;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 20px;
|
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);
|
border-bottom: 1px solid var(--line);
|
||||||
font: 700 var(--step-0) var(--font-mono);
|
font: 700 var(--step-0) var(--font-mono);
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
@@ -61,4 +68,13 @@ const { id } = Astro.props;
|
|||||||
display: none;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
// Static shell: the SVG paths render server-side; the nodes are buttons with
|
// 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 `data-tree` hook asserted by `scripts/verify.mjs`. The `root` node and
|
||||||
// the `initial` branch are marked selected.
|
// 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 };
|
type Localized = { en: string; pt: string };
|
||||||
|
|
||||||
@@ -26,23 +31,37 @@ interface Props {
|
|||||||
initial?: string;
|
initial?: string;
|
||||||
rootLabel?: string | Localized;
|
rootLabel?: string | Localized;
|
||||||
rootSmall?: 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">
|
<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 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 C380 170 110 150 110 224"></path>
|
||||||
<path class="tree-edge" d="M380 118 V224"></path>
|
<path class="tree-edge" d="M380 118 V224"></path>
|
||||||
<path class="tree-edge" d="M380 118 C380 170 650 150 650 224"></path>
|
<path class="tree-edge" d="M380 118 C380 170 650 150 650 224"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<button
|
<Node
|
||||||
class:list={['tree-node', 'root', { active: initial === 'main' }]}
|
class:list={['tree-node', 'root', { active: initial === 'main' }]}
|
||||||
data-tree="main"
|
data-tree={interactive ? 'main' : undefined}
|
||||||
role="treeitem"
|
role={interactive ? 'treeitem' : undefined}
|
||||||
aria-selected={initial === 'main'}
|
aria-selected={interactive ? initial === 'main' : undefined}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
@@ -73,14 +92,14 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</Node>
|
||||||
{
|
{
|
||||||
branches.map((branch) => (
|
branches.map((branch) => (
|
||||||
<button
|
<Node
|
||||||
class:list={['tree-node', 'branch', branch.tone, { active: branch.id === initial }]}
|
class:list={['tree-node', 'branch', branch.tone, { active: branch.id === initial }]}
|
||||||
data-tree={branch.id}
|
data-tree={interactive ? branch.id : undefined}
|
||||||
role="treeitem"
|
role={interactive ? 'treeitem' : undefined}
|
||||||
aria-selected={branch.id === initial}
|
aria-selected={interactive ? branch.id === initial : undefined}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
@@ -109,7 +128,7 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
|||||||
)}
|
)}
|
||||||
</small>
|
</small>
|
||||||
</>
|
</>
|
||||||
</button>
|
</Node>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -135,6 +154,10 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
|||||||
fill: none;
|
fill: none;
|
||||||
stroke: var(--gold);
|
stroke: var(--gold);
|
||||||
stroke-width: 2;
|
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. */
|
/* Root sits at the top centre, spanning both columns. */
|
||||||
@@ -190,6 +213,106 @@ const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean'
|
|||||||
outline-offset: -3px;
|
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. */
|
/* Branch tone variants — left/right placement stays on the grid columns. */
|
||||||
.tree-node.branch.ui {
|
.tree-node.branch.ui {
|
||||||
grid-column: 1;
|
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="#pipeline" data-copy="navPipeline">{copy.en.navPipeline}</a>
|
||||||
<a href="#skills" data-copy="navSkills">{copy.en.navSkills}</a>
|
<a href="#skills" data-copy="navSkills">{copy.en.navSkills}</a>
|
||||||
<a href="#examples" data-copy="navExamples">{copy.en.navExamples}</a>
|
<a href="#examples" data-copy="navExamples">{copy.en.navExamples}</a>
|
||||||
|
<a href="#recall" data-copy="navRecall">{copy.en.navRecall}</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="languages" aria-label="Language">
|
<div class="languages" aria-label="Language">
|
||||||
<button class="active" data-lang="en" aria-pressed="true">EN</button>
|
<button class="active" data-lang="en" aria-pressed="true">EN</button>
|
||||||
@@ -51,10 +52,10 @@ import prompts from '../../content/rules/prompts.json';
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section class="hero">
|
<section class="hero rise-in">
|
||||||
<p class="eyebrow" data-copy="heroEyebrow">{copy.en.heroEyebrow}</p>
|
<p class="eyebrow" data-copy="heroEyebrow">{copy.en.heroEyebrow}</p>
|
||||||
<h1 data-copy="heroTitle" set:html={copy.en.heroTitle} />
|
<h1 class="parallax-near" data-copy="heroTitle" set:html={copy.en.heroTitle} />
|
||||||
<div class="hero-foot">
|
<div class="hero-foot parallax-far">
|
||||||
<p data-copy="heroText">{copy.en.heroText}</p>
|
<p data-copy="heroText">{copy.en.heroText}</p>
|
||||||
<aside>
|
<aside>
|
||||||
<span>CASE / NETCRACKER</span>
|
<span>CASE / NETCRACKER</span>
|
||||||
@@ -64,12 +65,12 @@ import prompts from '../../content/rules/prompts.json';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="thesis">
|
<section class="thesis reveal">
|
||||||
<span data-copy="thesisLabel">{copy.en.thesisLabel}</span>
|
<span data-copy="thesisLabel">{copy.en.thesisLabel}</span>
|
||||||
<strong data-copy="thesis">{copy.en.thesis}</strong>
|
<strong data-copy="thesis">{copy.en.thesis}</strong>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pipeline-section" id="pipeline">
|
<section class="pipeline-section reveal" id="pipeline">
|
||||||
<div class="section-label">
|
<div class="section-label">
|
||||||
<span data-copy="pipelineLabel">{copy.en.pipelineLabel}</span>
|
<span data-copy="pipelineLabel">{copy.en.pipelineLabel}</span>
|
||||||
<span data-copy="pipelineMeta">{copy.en.pipelineMeta}</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>
|
<article class="stage-detail" id="stage-detail" aria-live="polite"></article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="skill-section" id="skills">
|
<section class="skill-section reveal" id="skills">
|
||||||
<div class="section-label">
|
<div class="section-label">
|
||||||
<span data-copy="skillsLabel">{copy.en.skillsLabel}</span>
|
<span data-copy="skillsLabel">{copy.en.skillsLabel}</span>
|
||||||
<span data-copy="skillsMeta">{copy.en.skillsMeta}</span>
|
<span data-copy="skillsMeta">{copy.en.skillsMeta}</span>
|
||||||
@@ -158,7 +159,7 @@ import prompts from '../../content/rules/prompts.json';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="examples" id="examples">
|
<section class="examples reveal" id="examples">
|
||||||
<div class="section-label">
|
<div class="section-label">
|
||||||
<span data-copy="examplesLabel">{copy.en.examplesLabel}</span>
|
<span data-copy="examplesLabel">{copy.en.examplesLabel}</span>
|
||||||
<span data-copy="examplesMeta">{copy.en.examplesMeta}</span>
|
<span data-copy="examplesMeta">{copy.en.examplesMeta}</span>
|
||||||
@@ -223,7 +224,47 @@ fix(api): scope session query</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<div>
|
||||||
<span data-copy="copyLabel">{copy.en.copyLabel}</span>
|
<span data-copy="copyLabel">{copy.en.copyLabel}</span>
|
||||||
<h2 data-copy="copyTitle">{copy.en.copyTitle}</h2>
|
<h2 data-copy="copyTitle">{copy.en.copyTitle}</h2>
|
||||||
@@ -239,7 +280,7 @@ fix(api): scope session query</code></pre>
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="deeper">
|
<section class="deeper reveal">
|
||||||
<div>
|
<div>
|
||||||
<span data-copy="deeperLabel">{copy.en.deeperLabel}</span>
|
<span data-copy="deeperLabel">{copy.en.deeperLabel}</span>
|
||||||
<strong data-copy="deeperTitle">{copy.en.deeperTitle}</strong>
|
<strong data-copy="deeperTitle">{copy.en.deeperTitle}</strong>
|
||||||
@@ -413,6 +454,7 @@ fix(api): scope session query</code></pre>
|
|||||||
pipelineTitle: 1,
|
pipelineTitle: 1,
|
||||||
skillsTitle: 1,
|
skillsTitle: 1,
|
||||||
ratchetTitle: 1,
|
ratchetTitle: 1,
|
||||||
|
recallTitle: 1,
|
||||||
navPipeline: 1,
|
navPipeline: 1,
|
||||||
navSkills: 1,
|
navSkills: 1,
|
||||||
navExamples: 1,
|
navExamples: 1,
|
||||||
@@ -683,6 +725,7 @@ fix(api): scope session query</code></pre>
|
|||||||
.pipeline-section,
|
.pipeline-section,
|
||||||
.skill-section,
|
.skill-section,
|
||||||
.examples,
|
.examples,
|
||||||
|
.recall,
|
||||||
.copy-lab {
|
.copy-lab {
|
||||||
margin-bottom: 140px;
|
margin-bottom: 140px;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
@@ -952,6 +995,73 @@ fix(api): scope session query</code></pre>
|
|||||||
color: var(--blue);
|
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 {
|
.copy-lab {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 0.75fr 1.25fr;
|
grid-template-columns: 0.75fr 1.25fr;
|
||||||
@@ -998,6 +1108,15 @@ fix(api): scope session query</code></pre>
|
|||||||
600 9px 'DM Mono',
|
600 9px 'DM Mono',
|
||||||
monospace;
|
monospace;
|
||||||
cursor: pointer;
|
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 {
|
.copy-lab pre {
|
||||||
padding: 26px;
|
padding: 26px;
|
||||||
@@ -1070,6 +1189,7 @@ fix(api): scope session query</code></pre>
|
|||||||
.pipeline button:focus-visible,
|
.pipeline button:focus-visible,
|
||||||
.skill-list button:focus-visible,
|
.skill-list button:focus-visible,
|
||||||
.copy-lab button:focus-visible,
|
.copy-lab button:focus-visible,
|
||||||
|
.recall-grid summary:focus-visible,
|
||||||
.deeper a:focus-visible {
|
.deeper a:focus-visible {
|
||||||
outline: 3px solid var(--gold);
|
outline: 3px solid var(--gold);
|
||||||
outline-offset: -3px;
|
outline-offset: -3px;
|
||||||
@@ -1147,7 +1267,8 @@ fix(api): scope session query</code></pre>
|
|||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-template-rows: none;
|
grid-template-rows: none;
|
||||||
}
|
}
|
||||||
.example-grid {
|
.example-grid,
|
||||||
|
.recall-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.deeper nav {
|
.deeper nav {
|
||||||
@@ -1240,7 +1361,9 @@ fix(api): scope session query</code></pre>
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
.pipeline button,
|
.pipeline button,
|
||||||
.skill-list button {
|
.skill-list button,
|
||||||
|
.copy-lab button,
|
||||||
|
.recall-grid summary b {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
.stage-detail > *,
|
.stage-detail > *,
|
||||||
|
|||||||
@@ -89,9 +89,24 @@ const packageFiles: PackageFile[] = [
|
|||||||
const preview = document.querySelector('#package-preview');
|
const preview = document.querySelector('#package-preview');
|
||||||
const buttons = document.querySelectorAll('[data-skill-file]');
|
const buttons = document.querySelectorAll('[data-skill-file]');
|
||||||
const dataNode = document.querySelector('[data-skill-files]');
|
const dataNode = document.querySelector('[data-skill-files]');
|
||||||
|
const tree = document.querySelector('[data-package-workbench] .package-tree');
|
||||||
if (!preview || !dataNode) return;
|
if (!preview || !dataNode) return;
|
||||||
const files = JSON.parse(dataNode.textContent || '[]');
|
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) {
|
function renderPackage(id) {
|
||||||
const item = files.find((entry) => entry.id === id);
|
const item = files.find((entry) => entry.id === id);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
@@ -116,6 +131,7 @@ const packageFiles: PackageFile[] = [
|
|||||||
button.classList.toggle('active', active);
|
button.classList.toggle('active', active);
|
||||||
button.setAttribute('aria-selected', String(active));
|
button.setAttribute('aria-selected', String(active));
|
||||||
});
|
});
|
||||||
|
placeIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.forEach(function (button) {
|
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');
|
renderPackage('skill');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
@@ -139,11 +159,51 @@ const packageFiles: PackageFile[] = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.package-tree {
|
.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;
|
padding: 22px 16px;
|
||||||
border-right: 1px solid var(--muted);
|
border-right: 1px solid var(--muted);
|
||||||
min-width: 0;
|
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-tree > p,
|
||||||
.package-preview > span {
|
.package-preview > span {
|
||||||
margin: 0 0 14px;
|
margin: 0 0 14px;
|
||||||
@@ -167,16 +227,24 @@ const packageFiles: PackageFile[] = [
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
background 0.2s ease,
|
background 0.2s ease,
|
||||||
border-color 0.2s ease,
|
|
||||||
transform 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:hover,
|
||||||
.package-tree button:focus-visible,
|
.package-tree button:focus-visible,
|
||||||
.package-tree button.active {
|
.package-tree button.active {
|
||||||
border-left-color: var(--gold);
|
|
||||||
background: var(--blue);
|
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 {
|
.package-tree button:hover {
|
||||||
|
|||||||
@@ -1,31 +1,55 @@
|
|||||||
{
|
{
|
||||||
"id": "agents",
|
"id": "agents",
|
||||||
"eyebrow": { "en": "Subagent workflow", "pt": "Subagent workflow" },
|
"eyebrow": {
|
||||||
"title": { "en": "One branch<br>per <em>hand.</em>", "pt": "One branch<br>per <em>hand.</em>" },
|
"en": "Subagent workflow",
|
||||||
|
"pt": "Subagent workflow"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "One branch<br>per <em>hand.</em>",
|
||||||
|
"pt": "One branch<br>per <em>hand.</em>"
|
||||||
|
},
|
||||||
"lede": {
|
"lede": {
|
||||||
"en": "Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent.",
|
"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."
|
"pt": "Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent."
|
||||||
},
|
},
|
||||||
"cards": [
|
"cards": [
|
||||||
{
|
{
|
||||||
"label": { "en": "FRAME", "pt": "FRAME" },
|
"label": {
|
||||||
"title": { "en": "Orchestrator", "pt": "Orchestrator" },
|
"en": "FRAME",
|
||||||
|
"pt": "FRAME"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Orchestrator",
|
||||||
|
"pt": "Orchestrator"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Owns scope, task graph, boundaries, and integration.",
|
"en": "Owns scope, task graph, boundaries, and integration.",
|
||||||
"pt": "Owns scope, task graph, boundaries, and integration."
|
"pt": "Owns scope, task graph, boundaries, and integration."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": { "en": "HAND OFF", "pt": "HAND OFF" },
|
"label": {
|
||||||
"title": { "en": "Worker", "pt": "Worker" },
|
"en": "HAND OFF",
|
||||||
|
"pt": "HAND OFF"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Worker",
|
||||||
|
"pt": "Worker"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Owns one coherent slice and one worktree.",
|
"en": "Owns one coherent slice and one worktree.",
|
||||||
"pt": "Owns one coherent slice and one worktree."
|
"pt": "Owns one coherent slice and one worktree."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": { "en": "PROVE", "pt": "PROVE" },
|
"label": {
|
||||||
"title": { "en": "Verifier", "pt": "Verifier" },
|
"en": "PROVE",
|
||||||
|
"pt": "PROVE"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Verifier",
|
||||||
|
"pt": "Verifier"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Re-runs gates and reports remaining gaps.",
|
"en": "Re-runs gates and reports remaining gaps.",
|
||||||
"pt": "Re-runs gates and reports remaining gaps."
|
"pt": "Re-runs gates and reports remaining gaps."
|
||||||
@@ -34,37 +58,58 @@
|
|||||||
],
|
],
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"eyebrow": { "en": "The tree", "pt": "The tree" },
|
"eyebrow": {
|
||||||
"title": { "en": "Split at<br>the <em>seam.</em>", "pt": "Split at<br>the <em>seam.</em>" },
|
"en": "The tree",
|
||||||
"panelLabel": { "en": "MAIN / ORCHESTRATOR", "pt": "MAIN / ORCHESTRATOR" },
|
"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": {
|
"panelCode": {
|
||||||
"en": "├── 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 · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge 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": {
|
"title": {
|
||||||
"en": "Context that<br>can <em>travel.</em>",
|
"en": "Context that<br>can <em>travel.</em>",
|
||||||
"pt": "Context that<br>can <em>travel.</em>"
|
"pt": "Context that<br>can <em>travel.</em>"
|
||||||
},
|
},
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"label": { "en": "Brief", "pt": "Brief" },
|
"label": {
|
||||||
|
"en": "Brief",
|
||||||
|
"pt": "Brief"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Goal, owned files, dependencies, non-goals, acceptance.",
|
"en": "Goal, owned files, dependencies, non-goals, acceptance.",
|
||||||
"pt": "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": {
|
"copy": {
|
||||||
"en": "One branch and worktree per independent change.",
|
"en": "One branch and worktree per independent change.",
|
||||||
"pt": "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": {
|
"copy": {
|
||||||
"en": "Commands, result, changed files, screenshots, gaps.",
|
"en": "Commands, result, changed files, screenshots, gaps.",
|
||||||
"pt": "Commands, result, changed files, screenshots, gaps."
|
"pt": "Commands, result, changed files, screenshots, gaps."
|
||||||
|
|||||||
@@ -1,31 +1,55 @@
|
|||||||
{
|
{
|
||||||
"id": "models",
|
"id": "models",
|
||||||
"eyebrow": { "en": "Model routing", "pt": "Model routing" },
|
"eyebrow": {
|
||||||
"title": { "en": "Choose the<br><em>engine.</em>", "pt": "Choose the<br><em>engine.</em>" },
|
"en": "Model routing",
|
||||||
|
"pt": "Model routing"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Choose the<br><em>engine.</em>",
|
||||||
|
"pt": "Choose the<br><em>engine.</em>"
|
||||||
|
},
|
||||||
"lede": {
|
"lede": {
|
||||||
"en": "A model has a capability ceiling. Effort controls how much room it gets to reason. Route by uncertainty and verification cost.",
|
"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."
|
"pt": "A model has a capability ceiling. Effort controls how much room it gets to reason. Route by uncertainty and verification cost."
|
||||||
},
|
},
|
||||||
"cards": [
|
"cards": [
|
||||||
{
|
{
|
||||||
"label": { "en": "LOW", "pt": "LOW" },
|
"label": {
|
||||||
"title": { "en": "Bounded rhythm", "pt": "Bounded rhythm" },
|
"en": "LOW",
|
||||||
|
"pt": "LOW"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Bounded rhythm",
|
||||||
|
"pt": "Bounded rhythm"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Lookup, small edits, formatting, and transformations with clear checks.",
|
"en": "Lookup, small edits, formatting, and transformations with clear checks.",
|
||||||
"pt": "Lookup, small edits, formatting, and transformations with clear checks."
|
"pt": "Lookup, small edits, formatting, and transformations with clear checks."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": { "en": "MEDIUM", "pt": "MEDIUM" },
|
"label": {
|
||||||
"title": { "en": "Default work", "pt": "Default work" },
|
"en": "MEDIUM",
|
||||||
|
"pt": "MEDIUM"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Default work",
|
||||||
|
"pt": "Default work"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Normal implementation where the contract is clear but context matters.",
|
"en": "Normal implementation where the contract is clear but context matters.",
|
||||||
"pt": "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" },
|
"label": {
|
||||||
"title": { "en": "Ambiguity", "pt": "Ambiguity" },
|
"en": "HIGH",
|
||||||
|
"pt": "HIGH"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Ambiguity",
|
||||||
|
"pt": "Ambiguity"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Planning, architecture, security judgment, and hard failures.",
|
"en": "Planning, architecture, security judgment, and hard failures.",
|
||||||
"pt": "Planning, architecture, security judgment, and hard failures."
|
"pt": "Planning, architecture, security judgment, and hard failures."
|
||||||
@@ -34,37 +58,58 @@
|
|||||||
],
|
],
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"eyebrow": { "en": "Two knobs", "pt": "Two knobs" },
|
"eyebrow": {
|
||||||
"title": { "en": "Capability<br>× effort", "pt": "Capability<br>× effort" },
|
"en": "Two knobs",
|
||||||
"panelLabel": { "en": "ROUTING RULE", "pt": "ROUTING RULE" },
|
"pt": "Two knobs"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Capability<br>× effort",
|
||||||
|
"pt": "Capability<br>× effort"
|
||||||
|
},
|
||||||
|
"panelLabel": {
|
||||||
|
"en": "ROUTING RULE",
|
||||||
|
"pt": "ROUTING RULE"
|
||||||
|
},
|
||||||
"panelCode": {
|
"panelCode": {
|
||||||
"en": "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 · light model + low effort → bounded execution · raise 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": {
|
"title": {
|
||||||
"en": "Spend judgment<br>where it <em>compounds.</em>",
|
"en": "Spend judgment<br>where it <em>compounds.</em>",
|
||||||
"pt": "Spend judgment<br>where it <em>compounds.</em>"
|
"pt": "Spend judgment<br>where it <em>compounds.</em>"
|
||||||
},
|
},
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"label": { "en": "Plan", "pt": "Plan" },
|
"label": {
|
||||||
|
"en": "Plan",
|
||||||
|
"pt": "Plan"
|
||||||
|
},
|
||||||
"copy": {
|
"copy": {
|
||||||
"en": "Strong model: scope, risks, acceptance, and worktree split.",
|
"en": "Strong model: scope, risks, acceptance, and worktree split.",
|
||||||
"pt": "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": {
|
"copy": {
|
||||||
"en": "Focused worker: smallest context and lightest model that can pass.",
|
"en": "Focused worker: smallest context and lightest model that can pass.",
|
||||||
"pt": "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": {
|
"copy": {
|
||||||
"en": "Independent pass when missed issues cost more than the call.",
|
"en": "Independent pass when missed issues cost more than the call.",
|
||||||
"pt": "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
|
// rule panels on /models/, /agents/, and /skills/ (the routing rule, the
|
||||||
// MAIN / ORCHESTRATOR tree, and the package-anatomy hint).
|
// 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
|
// `threadLabel` / `threadText` and `footer` carry the landing-page thread
|
||||||
// strip and footer line.
|
// strip and footer line.
|
||||||
const chapters = defineCollection({
|
const chapters = defineCollection({
|
||||||
@@ -147,6 +153,22 @@ const chapters = defineCollection({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
recall: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
question: localized,
|
||||||
|
answer: localized,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
links: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
label: localized,
|
||||||
|
href: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"navPipeline": "Pipeline",
|
"navPipeline": "Pipeline",
|
||||||
"navSkills": "Skills",
|
"navSkills": "Skills",
|
||||||
"navExamples": "Examples",
|
"navExamples": "Examples",
|
||||||
|
"navRecall": "Recall",
|
||||||
"stageContext": "CONTEXT",
|
"stageContext": "CONTEXT",
|
||||||
"stageReview": "REVIEW",
|
"stageReview": "REVIEW",
|
||||||
"heroEyebrow": "A real repository case study",
|
"heroEyebrow": "A real repository case study",
|
||||||
@@ -45,6 +46,17 @@
|
|||||||
"copyTitle": "Ask your agent to map the enforcement stack.",
|
"copyTitle": "Ask your agent to map the enforcement stack.",
|
||||||
"copyText": "Use this in the interview repository or adapt the path names to another project.",
|
"copyText": "Use this in the interview repository or adapt the path names to another project.",
|
||||||
"copyButton": "COPY PROMPT",
|
"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",
|
"deeperLabel": "GO DEEPER",
|
||||||
"deeperTitle": "Read the implementation, not just this summary.",
|
"deeperTitle": "Read the implementation, not just this summary.",
|
||||||
"deepContext": "Repository context",
|
"deepContext": "Repository context",
|
||||||
@@ -66,6 +78,7 @@
|
|||||||
"navPipeline": "Pipeline",
|
"navPipeline": "Pipeline",
|
||||||
"navSkills": "Skills",
|
"navSkills": "Skills",
|
||||||
"navExamples": "Exemplos",
|
"navExamples": "Exemplos",
|
||||||
|
"navRecall": "Recall",
|
||||||
"stageContext": "CONTEXTO",
|
"stageContext": "CONTEXTO",
|
||||||
"stageReview": "REVISÃO",
|
"stageReview": "REVISÃO",
|
||||||
"heroEyebrow": "Um estudo de caso de repositório real",
|
"heroEyebrow": "Um estudo de caso de repositório real",
|
||||||
@@ -107,6 +120,17 @@
|
|||||||
"copyTitle": "Peça ao agente para mapear o enforcement.",
|
"copyTitle": "Peça ao agente para mapear o enforcement.",
|
||||||
"copyText": "Use isto no repositório interview ou adapte os caminhos para outro projeto.",
|
"copyText": "Use isto no repositório interview ou adapte os caminhos para outro projeto.",
|
||||||
"copyButton": "COPIAR PROMPT",
|
"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",
|
"deeperLabel": "APROFUNDE",
|
||||||
"deeperTitle": "Leia a implementação, não apenas este resumo.",
|
"deeperTitle": "Leia a implementação, não apenas este resumo.",
|
||||||
"deepContext": "Contexto do repositório",
|
"deepContext": "Contexto do repositório",
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import BaseLayout from './BaseLayout.astro';
|
|||||||
import TopBar from '../components/blocks/TopBar.astro';
|
import TopBar from '../components/blocks/TopBar.astro';
|
||||||
import SiteFooter from '../components/blocks/SiteFooter.astro';
|
import SiteFooter from '../components/blocks/SiteFooter.astro';
|
||||||
import chaptersStylesheet from '../../legacy/styles/chapters.css?url';
|
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 {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -26,6 +29,7 @@ const { title, description, lang = 'en' } = Astro.props;
|
|||||||
|
|
||||||
<BaseLayout title={title} description={description} lang={lang}>
|
<BaseLayout title={title} description={description} lang={lang}>
|
||||||
<link slot="styles" rel="stylesheet" href={chaptersStylesheet} />
|
<link slot="styles" rel="stylesheet" href={chaptersStylesheet} />
|
||||||
|
<link slot="styles" rel="stylesheet" href={motionStylesheet} />
|
||||||
<TopBar id="top">
|
<TopBar id="top">
|
||||||
<slot name="top-previous" slot="previous" />
|
<slot name="top-previous" slot="previous" />
|
||||||
<slot name="top-center" slot="center" />
|
<slot name="top-center" slot="center" />
|
||||||
|
|||||||
+59
-20
@@ -3,9 +3,12 @@
|
|||||||
// Identical URL (/agents/), zero client JS, copy lives in
|
// Identical URL (/agents/), zero client JS, copy lives in
|
||||||
// src/content/chapters/agents.json.
|
// src/content/chapters/agents.json.
|
||||||
|
|
||||||
import { getEntry } from 'astro:content';
|
import { getCollection, getEntry } from 'astro:content';
|
||||||
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||||
import ChapterHero from '../components/blocks/ChapterHero.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 /
|
// Required-field guard. The chapters schema marks section eyebrow /
|
||||||
// panelLabel / panelCode / steps / copy as optional because the schema
|
// 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 treePanelCode = requireField(treeSection.panelCode, 'sections[0].panelCode');
|
||||||
const handoffEyebrow = requireField(handoffSection.eyebrow, 'sections[1].eyebrow');
|
const handoffEyebrow = requireField(handoffSection.eyebrow, 'sections[1].eyebrow');
|
||||||
const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
|
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, '')}>
|
<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>
|
<p>{lede}</p>
|
||||||
</ChapterHero>
|
</ChapterHero>
|
||||||
|
|
||||||
<section class="pipeline">
|
<section class="pipeline reveal">
|
||||||
<div>
|
<div class="depth-slow">
|
||||||
<p class="eyebrow">{treeEyebrow.en}</p>
|
<p class="eyebrow">{treeEyebrow.en}</p>
|
||||||
<h2 set:html={treeSection.title.en} />
|
<h2 set:html={treeSection.title.en} />
|
||||||
|
<DrawRule shape="bracket" height={110} />
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel depth-fast">
|
||||||
<strong>{treePanelLabel.en}</strong>
|
<strong>{treePanelLabel.en}</strong>
|
||||||
<code>{treePanelCode.en}</code>
|
<code>{treePanelCode.en}</code>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<section class="grid">
|
<section class="grid reveal">
|
||||||
{
|
{
|
||||||
cards.map((card) => (
|
cards.map((card) => (
|
||||||
<article class="card">
|
<article class="card">
|
||||||
@@ -78,24 +110,16 @@ const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
|
|||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="practice">
|
<section class="practice reveal">
|
||||||
<div>
|
<div class="depth-slow">
|
||||||
<p class="eyebrow">{handoffEyebrow.en}</p>
|
<p class="eyebrow">{handoffEyebrow.en}</p>
|
||||||
<h2 set:html={handoffSection.title.en} />
|
<h2 set:html={handoffSection.title.en} />
|
||||||
|
<DrawRule height={110} />
|
||||||
</div>
|
</div>
|
||||||
<div class="steps">
|
<StepFlow
|
||||||
{
|
class="depth-fast"
|
||||||
handoffSteps.map((step, index) => (
|
steps={handoffSteps.map((step) => ({ label: step.label.en, copy: step.copy.en }))}
|
||||||
<article>
|
/>
|
||||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
|
||||||
<div>
|
|
||||||
<strong>{step.label.en}</strong>
|
|
||||||
<span>{step.copy.en}</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
|
<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>
|
<a href={`${base}hands-on/rules/`}>Try the rules lab →</a>
|
||||||
</nav>
|
</nav>
|
||||||
</ChapterLayout>
|
</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>
|
</ChapterHero>
|
||||||
|
|
||||||
<GridGroup label="Guide chapters" columns={3}>
|
<GridGroup label="Guide chapters" columns={3} class="reveal">
|
||||||
{
|
{
|
||||||
cards.map((card) => (
|
cards.map((card) => (
|
||||||
<RouteCard
|
<RouteCard
|
||||||
@@ -62,7 +62,7 @@ const cards = data.cards ?? [];
|
|||||||
}
|
}
|
||||||
</GridGroup>
|
</GridGroup>
|
||||||
|
|
||||||
<section class="landing-note">
|
<section class="landing-note reveal">
|
||||||
<span>{data.threadLabel?.[lang]}</span>
|
<span>{data.threadLabel?.[lang]}</span>
|
||||||
<strong>{data.threadText?.[lang]}</strong>
|
<strong>{data.threadText?.[lang]}</strong>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+23
-19
@@ -7,6 +7,8 @@
|
|||||||
import { getEntry } from 'astro:content';
|
import { getEntry } from 'astro:content';
|
||||||
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||||
import ChapterHero from '../components/blocks/ChapterHero.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 /
|
// Required-field guard. The chapters schema marks section eyebrow /
|
||||||
// panelLabel / panelCode / steps / copy as optional because the schema
|
// panelLabel / panelCode / steps / copy as optional because the schema
|
||||||
@@ -55,7 +57,7 @@ const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
|
|||||||
<p>{lede}</p>
|
<p>{lede}</p>
|
||||||
</ChapterHero>
|
</ChapterHero>
|
||||||
|
|
||||||
<section class="grid">
|
<section class="grid reveal">
|
||||||
{
|
{
|
||||||
cards.map((card) => (
|
cards.map((card) => (
|
||||||
<article class="card">
|
<article class="card">
|
||||||
@@ -67,35 +69,28 @@ const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
|
|||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="model">
|
<section class="model reveal">
|
||||||
<div>
|
<div class="depth-slow">
|
||||||
<p class="eyebrow">{ruleEyebrow.en}</p>
|
<p class="eyebrow">{ruleEyebrow.en}</p>
|
||||||
<h2 set:html={ruleSection.title.en} />
|
<h2 set:html={ruleSection.title.en} />
|
||||||
|
<DrawRule shape="bracket" height={110} />
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel depth-fast">
|
||||||
<strong>{rulePanelLabel.en}</strong>
|
<strong>{rulePanelLabel.en}</strong>
|
||||||
<code>{rulePanelCode.en}</code>
|
<code>{rulePanelCode.en}</code>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="practice">
|
<section class="practice reveal">
|
||||||
<div>
|
<div class="depth-slow">
|
||||||
<p class="eyebrow">{sequenceEyebrow.en}</p>
|
<p class="eyebrow">{sequenceEyebrow.en}</p>
|
||||||
<h2 set:html={sequenceSection.title.en} />
|
<h2 set:html={sequenceSection.title.en} />
|
||||||
|
<DrawRule height={110} />
|
||||||
</div>
|
</div>
|
||||||
<div class="steps">
|
<StepFlow
|
||||||
{
|
class="depth-fast"
|
||||||
sequenceSteps.map((step, index) => (
|
steps={sequenceSteps.map((step) => ({ label: step.label.en, copy: step.copy.en }))}
|
||||||
<article>
|
/>
|
||||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
|
||||||
<div>
|
|
||||||
<strong>{step.label.en}</strong>
|
|
||||||
<span>{step.copy.en}</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
|
<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>
|
<a href={`${base}rules/`}>Rules case study →</a>
|
||||||
</nav>
|
</nav>
|
||||||
</ChapterLayout>
|
</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 BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import RulesInteractive from '../components/islands/RulesInteractive.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
|
<BaseLayout
|
||||||
title="Rules That Survive the Prompt — AI For Dummies"
|
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."
|
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 />
|
<RulesInteractive />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const serializedCatalog = JSON.stringify(catalog).replace(/</g, '\\u003c');
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 24px;
|
top: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
max-height: calc(100dvh - 48px);
|
max-height: calc(100dvh - 48px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -171,6 +172,12 @@ const serializedCatalog = JSON.stringify(catalog).replace(/</g, '\\u003c');
|
|||||||
|
|
||||||
#skill-list {
|
#skill-list {
|
||||||
min-height: 0;
|
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;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
scrollbar-color: var(--blue) var(--paper);
|
scrollbar-color: var(--blue) var(--paper);
|
||||||
|
|||||||
+214
-16
@@ -8,7 +8,17 @@ import { getEntry } from 'astro:content';
|
|||||||
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||||
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||||
import SkillPackageExplorer from '../components/islands/SkillPackageExplorer.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 /
|
// Required-field guard. The chapters schema marks section eyebrow /
|
||||||
// panelLabel / panelCode / steps / copy as optional because the schema
|
// 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 anatomyCopy = requireField(anatomySection.copy, 'sections[0].copy');
|
||||||
const createEyebrow = requireField(createSection.eyebrow, 'sections[1].eyebrow');
|
const createEyebrow = requireField(createSection.eyebrow, 'sections[1].eyebrow');
|
||||||
const createSteps = requireField(createSection.steps, 'sections[1].steps');
|
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, '')}>
|
<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>
|
<a slot="top-previous" href={`${base}summary/`}>← ROUTE MAP</a>
|
||||||
<span slot="top-center">03 / SKILLS</span>
|
<span slot="top-center">03 / SKILLS</span>
|
||||||
<a slot="top-next" href={`${base}skills-review/`}>review desk ↗</a>
|
<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} />
|
<p set:html={lede} />
|
||||||
</ChapterHero>
|
</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>
|
<p class="eyebrow">{anatomyEyebrow.en}</p>
|
||||||
<h2 set:html={anatomySection.title.en} />
|
<h2 set:html={anatomySection.title.en} />
|
||||||
<p class="package-hint">{anatomyCopy.en}</p>
|
<p class="package-hint">{anatomyCopy.en}</p>
|
||||||
|
<DrawRule shape="bracket" height={150} />
|
||||||
|
</div>
|
||||||
|
<div class="depth-fast">
|
||||||
|
<SkillPackageExplorer />
|
||||||
</div>
|
</div>
|
||||||
<SkillPackageExplorer />
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="practice">
|
<section class="practice reveal">
|
||||||
<div>
|
<div class="depth-slow">
|
||||||
<p class="eyebrow">{createEyebrow.en}</p>
|
<p class="eyebrow">{createEyebrow.en}</p>
|
||||||
<h2 set:html={createSection.title.en} />
|
<h2 set:html={createSection.title.en} />
|
||||||
|
<DrawRule height={110} />
|
||||||
</div>
|
</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) => (
|
recallItems.map((item) => (
|
||||||
<article>
|
<details>
|
||||||
<b>{String(index + 1).padStart(2, '0')}</b>
|
<summary>
|
||||||
<div>
|
<span>{item.question.en}</span>
|
||||||
<strong>{step.label.en}</strong>
|
<b aria-hidden="true">+</b>
|
||||||
<span>{step.copy.en}</span>
|
</summary>
|
||||||
</div>
|
<p>{item.answer.en}</p>
|
||||||
</article>
|
</details>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</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>
|
<a href={`${base}full-guide/#create-skill`}>Full guide: skill forge →</a>
|
||||||
</nav>
|
</nav>
|
||||||
</ChapterLayout>
|
</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>
|
<p>{introduction}</p>
|
||||||
</ChapterHero>
|
</ChapterHero>
|
||||||
|
|
||||||
<SectionGrid>
|
<SectionGrid class="reveal">
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<b>01</b><h2>Models</h2><p>Capability and effort are separate knobs.</p><a
|
<b>01</b><h2>Models</h2><p>Capability and effort are separate knobs.</p><a
|
||||||
href={`${base}models/`}>Open chapter →</a
|
href={`${base}models/`}>Open chapter →</a
|
||||||
|
|||||||
@@ -37,3 +37,15 @@ a:hover {
|
|||||||
transition: none;
|
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-rule: #ffffff32;
|
||||||
--guide-prompt-enhanced-copy: #e5e3ef;
|
--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 */
|
/* Breakpoints */
|
||||||
--bp-sm: 560px;
|
--bp-sm: 560px;
|
||||||
--bp-md: 800px;
|
--bp-md: 800px;
|
||||||
|
|||||||
Reference in New Issue
Block a user