{copy.en.heroEyebrow}
- -{copy.en.heroText}
{copy.en.recallEyebrow}
+ +{copy.en.recallText}
++ {copy.en.recallQ1}+ +
+{copy.en.recallA1}
++ {copy.en.recallQ2}+ +
+{copy.en.recallA2}
++ {copy.en.recallQ3}+ +
+{copy.en.recallA3}
+{copy.en.copyTitle}
@@ -239,7 +280,7 @@ fix(api): scope session query -per hand.", "pt": "One branch
per hand." }, + "eyebrow": { + "en": "Subagent workflow", + "pt": "Subagent workflow" + }, + "title": { + "en": "One branch
per hand.", + "pt": "One branch
per hand." + }, "lede": { "en": "Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent.", "pt": "Agents work when roles, files, and evidence are bounded. A worktree gives each worker its own checkout while the orchestrator protects intent." }, "cards": [ { - "label": { "en": "FRAME", "pt": "FRAME" }, - "title": { "en": "Orchestrator", "pt": "Orchestrator" }, + "label": { + "en": "FRAME", + "pt": "FRAME" + }, + "title": { + "en": "Orchestrator", + "pt": "Orchestrator" + }, "copy": { "en": "Owns scope, task graph, boundaries, and integration.", "pt": "Owns scope, task graph, boundaries, and integration." } }, { - "label": { "en": "HAND OFF", "pt": "HAND OFF" }, - "title": { "en": "Worker", "pt": "Worker" }, + "label": { + "en": "HAND OFF", + "pt": "HAND OFF" + }, + "title": { + "en": "Worker", + "pt": "Worker" + }, "copy": { "en": "Owns one coherent slice and one worktree.", "pt": "Owns one coherent slice and one worktree." } }, { - "label": { "en": "PROVE", "pt": "PROVE" }, - "title": { "en": "Verifier", "pt": "Verifier" }, + "label": { + "en": "PROVE", + "pt": "PROVE" + }, + "title": { + "en": "Verifier", + "pt": "Verifier" + }, "copy": { "en": "Re-runs gates and reports remaining gaps.", "pt": "Re-runs gates and reports remaining gaps." @@ -34,37 +58,58 @@ ], "sections": [ { - "eyebrow": { "en": "The tree", "pt": "The tree" }, - "title": { "en": "Split at
the seam.", "pt": "Split at
the seam." }, - "panelLabel": { "en": "MAIN / ORCHESTRATOR", "pt": "MAIN / ORCHESTRATOR" }, + "eyebrow": { + "en": "The tree", + "pt": "The tree" + }, + "title": { + "en": "Split at
the seam.", + "pt": "Split at
the seam." + }, + "panelLabel": { + "en": "MAIN / ORCHESTRATOR", + "pt": "MAIN / ORCHESTRATOR" + }, "panelCode": { - "en": "├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence", - "pt": "├── agent/ui → components + visual states · ├── agent/tests → acceptance + regressions · └── agent/docs → guide + examples · merge after each leaf returns a diff and evidence" + "en": "├── agent/ui → components + visual states\n├── agent/tests → acceptance + regressions\n└── agent/docs → guide + examples\n\nmerge after each leaf returns a diff and evidence", + "pt": "├── agent/ui → components + visual states\n├── agent/tests → acceptance + regressions\n└── agent/docs → guide + examples\n\nmerge after each leaf returns a diff and evidence" } }, { - "eyebrow": { "en": "Handoff", "pt": "Handoff" }, + "eyebrow": { + "en": "Handoff", + "pt": "Handoff" + }, "title": { "en": "Context that
can travel.", "pt": "Context that
can travel." }, "steps": [ { - "label": { "en": "Brief", "pt": "Brief" }, + "label": { + "en": "Brief", + "pt": "Brief" + }, "copy": { "en": "Goal, owned files, dependencies, non-goals, acceptance.", "pt": "Goal, owned files, dependencies, non-goals, acceptance." } }, { - "label": { "en": "Isolation", "pt": "Isolation" }, + "label": { + "en": "Isolation", + "pt": "Isolation" + }, "copy": { "en": "One branch and worktree per independent change.", "pt": "One branch and worktree per independent change." } }, { - "label": { "en": "Evidence", "pt": "Evidence" }, + "label": { + "en": "Evidence", + "pt": "Evidence" + }, "copy": { "en": "Commands, result, changed files, screenshots, gaps.", "pt": "Commands, result, changed files, screenshots, gaps." diff --git a/src/content/chapters/models.json b/src/content/chapters/models.json index bc690af..48842df 100644 --- a/src/content/chapters/models.json +++ b/src/content/chapters/models.json @@ -1,31 +1,55 @@ { "id": "models", - "eyebrow": { "en": "Model routing", "pt": "Model routing" }, - "title": { "en": "Choose the
engine.", "pt": "Choose the
engine." }, + "eyebrow": { + "en": "Model routing", + "pt": "Model routing" + }, + "title": { + "en": "Choose the
engine.", + "pt": "Choose the
engine." + }, "lede": { "en": "A model has a capability ceiling. Effort controls how much room it gets to reason. Route by uncertainty and verification cost.", "pt": "A model has a capability ceiling. Effort controls how much room it gets to reason. Route by uncertainty and verification cost." }, "cards": [ { - "label": { "en": "LOW", "pt": "LOW" }, - "title": { "en": "Bounded rhythm", "pt": "Bounded rhythm" }, + "label": { + "en": "LOW", + "pt": "LOW" + }, + "title": { + "en": "Bounded rhythm", + "pt": "Bounded rhythm" + }, "copy": { "en": "Lookup, small edits, formatting, and transformations with clear checks.", "pt": "Lookup, small edits, formatting, and transformations with clear checks." } }, { - "label": { "en": "MEDIUM", "pt": "MEDIUM" }, - "title": { "en": "Default work", "pt": "Default work" }, + "label": { + "en": "MEDIUM", + "pt": "MEDIUM" + }, + "title": { + "en": "Default work", + "pt": "Default work" + }, "copy": { "en": "Normal implementation where the contract is clear but context matters.", "pt": "Normal implementation where the contract is clear but context matters." } }, { - "label": { "en": "HIGH", "pt": "HIGH" }, - "title": { "en": "Ambiguity", "pt": "Ambiguity" }, + "label": { + "en": "HIGH", + "pt": "HIGH" + }, + "title": { + "en": "Ambiguity", + "pt": "Ambiguity" + }, "copy": { "en": "Planning, architecture, security judgment, and hard failures.", "pt": "Planning, architecture, security judgment, and hard failures." @@ -34,37 +58,58 @@ ], "sections": [ { - "eyebrow": { "en": "Two knobs", "pt": "Two knobs" }, - "title": { "en": "Capability
× effort", "pt": "Capability
× effort" }, - "panelLabel": { "en": "ROUTING RULE", "pt": "ROUTING RULE" }, + "eyebrow": { + "en": "Two knobs", + "pt": "Two knobs" + }, + "title": { + "en": "Capability
× effort", + "pt": "Capability
× effort" + }, + "panelLabel": { + "en": "ROUTING RULE", + "pt": "ROUTING RULE" + }, "panelCode": { - "en": "strong model + high effort → frame ambiguity · light model + low effort → bounded execution · raise one knob at a time → compare evidence", - "pt": "strong model + high effort → frame ambiguity · light model + low effort → bounded execution · raise one knob at a time → compare evidence" + "en": "strong model + high effort → frame ambiguity\nlight model + low effort → bounded execution\nraise one knob at a time → compare evidence", + "pt": "strong model + high effort → frame ambiguity\nlight model + low effort → bounded execution\nraise one knob at a time → compare evidence" } }, { - "eyebrow": { "en": "Sequence", "pt": "Sequence" }, + "eyebrow": { + "en": "Sequence", + "pt": "Sequence" + }, "title": { "en": "Spend judgment
where it compounds.", "pt": "Spend judgment
where it compounds." }, "steps": [ { - "label": { "en": "Plan", "pt": "Plan" }, + "label": { + "en": "Plan", + "pt": "Plan" + }, "copy": { "en": "Strong model: scope, risks, acceptance, and worktree split.", "pt": "Strong model: scope, risks, acceptance, and worktree split." } }, { - "label": { "en": "Build", "pt": "Build" }, + "label": { + "en": "Build", + "pt": "Build" + }, "copy": { "en": "Focused worker: smallest context and lightest model that can pass.", "pt": "Focused worker: smallest context and lightest model that can pass." } }, { - "label": { "en": "Review", "pt": "Review" }, + "label": { + "en": "Review", + "pt": "Review" + }, "copy": { "en": "Independent pass when missed issues cost more than the call.", "pt": "Independent pass when missed issues cost more than the call." diff --git a/src/content/chapters/skills.json b/src/content/chapters/skills.json index 7847d2a..06e11f3 100644 --- a/src/content/chapters/skills.json +++ b/src/content/chapters/skills.json @@ -54,6 +54,62 @@ } } ] + }, + { + "eyebrow": { "en": "Check yourself", "pt": "Teste-se" }, + "title": { + "en": "Recall it before
you ship it.", + "pt": "Recupere antes
de enviar." + }, + "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/" + } + ] } ] } diff --git a/src/content/config.ts b/src/content/config.ts index 81d6aba..b2666b1 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -107,6 +107,12 @@ const skillInstallPrompts = defineCollection({ // rule panels on /models/, /agents/, and /skills/ (the routing rule, the // MAIN / ORCHESTRATOR tree, and the package-anatomy hint). // +// `sections[].recall` / `links` carry the retrieval-practice section on +// /skills/ ("check yourself"): `recall` holds question/answer pairs that +// render as native `
page. Recall.", + "recallText": "Retrieval practice — recalling an answer from memory before re-reading — is what builds long-term retention. Answer each question from memory first, then open it to check.", + "recallQ1": "Where does a rule belong: context, skill, CLI check, or hook?", + "recallA1": "Guidance the agent must discover goes in AGENTS.md or a skill. Deterministic policy becomes a CLI command, cheap gates run at commit time, and judgment calls go to review.", + "recallQ2": "Why does a skill need a trigger, not just a workflow?", + "recallA2": "The trigger says when to load it. Without one the skill either never fires or loads every time — and a skill that always loads is just a slower prompt.", + "recallQ3": "What separates a repository rule from a prompt?", + "recallA3": "The prompt is advice for one run. The rule is reusable context plus an executable boundary — still present when the conversation is gone.", "deeperLabel": "GO DEEPER", "deeperTitle": "Read the implementation, not just this summary.", "deepContext": "Repository context", @@ -66,6 +78,7 @@ "navPipeline": "Pipeline", "navSkills": "Skills", "navExamples": "Exemplos", + "navRecall": "Recall", "stageContext": "CONTEXTO", "stageReview": "REVISÃO", "heroEyebrow": "Um estudo de caso de repositório real", @@ -107,6 +120,17 @@ "copyTitle": "Peça ao agente para mapear o enforcement.", "copyText": "Use isto no repositório interview ou adapte os caminhos para outro projeto.", "copyButton": "COPIAR PROMPT", + "recallLabel": "Prática de recuperação", + "recallMeta": "responda antes de abrir", + "recallEyebrow": "Dificuldade desejável", + "recallTitle": "Feche o
guia. Recupere.", + "recallText": "Prática de recuperação — lembrar a resposta da memória antes de reler — é o que constrói retenção de longo prazo. Responda cada pergunta de memória primeiro, depois abra para conferir.", + "recallQ1": "Onde uma regra mora: contexto, skill, check de CLI ou hook?", + "recallA1": "Orientação que o agente precisa descobrir vai no AGENTS.md ou numa skill. Política determinística vira comando de CLI, checks baratos rodam no commit, e julgamento vai para a revisão.", + "recallQ2": "Por que uma skill precisa de um gatilho, não só de um workflow?", + "recallA2": "O gatilho diz quando carregá-la. Sem ele a skill nunca dispara — ou carrega sempre — e uma skill que sempre carrega é só um prompt mais lento.", + "recallQ3": "O que separa uma regra de repositório de um prompt?", + "recallA3": "O prompt orienta uma execução. A regra é contexto reutilizável mais uma fronteira executável — ainda presente quando a conversa acaba.", "deeperLabel": "APROFUNDE", "deeperTitle": "Leia a implementação, não apenas este resumo.", "deepContext": "Contexto do repositório", diff --git a/src/layouts/ChapterLayout.astro b/src/layouts/ChapterLayout.astro index b2347c3..860ff00 100644 --- a/src/layouts/ChapterLayout.astro +++ b/src/layouts/ChapterLayout.astro @@ -14,6 +14,9 @@ import BaseLayout from './BaseLayout.astro'; import TopBar from '../components/blocks/TopBar.astro'; import SiteFooter from '../components/blocks/SiteFooter.astro'; import chaptersStylesheet from '../../legacy/styles/chapters.css?url'; +// Shared motion layer (scroll reveals, hero parallax, load entrances). +// Opt-in classes; see src/styles/motion.css for the gating contract. +import motionStylesheet from '../styles/motion.css?url'; interface Props { title: string; @@ -26,6 +29,7 @@ const { title, description, lang = 'en' } = Astro.props;
{lede}
-{treeEyebrow.en}
+{treePanelCode.en}
{handoffEyebrow.en}
+{lede}
-{ruleEyebrow.en}
+{rulePanelCode.en}
{sequenceEyebrow.en}
+ overflowing the page instead of scrolling. Renamed off
+// `skills.css` at the same time so the two assets are told apart in a build
+// listing.
+import '../../legacy/styles/skills-chapter.css';
// Required-field guard. The chapters schema marks section eyebrow /
// panelLabel / panelCode / steps / copy as optional because the schema
@@ -42,10 +52,23 @@ const anatomyEyebrow = requireField(anatomySection.eyebrow, 'sections[0].eyebrow
const anatomyCopy = requireField(anatomySection.copy, 'sections[0].copy');
const createEyebrow = requireField(createSection.eyebrow, 'sections[1].eyebrow');
const createSteps = requireField(createSection.steps, 'sections[1].steps');
+// Retrieval-practice section ("check yourself") — teach-skill structure:
+// recall from memory, then open the answer for feedback. The citation and
+// practice-loop links ground the section in the spec and the hands-on lab.
+const recallSection = sections[2];
+if (!recallSection) {
+ throw new Error('skills chapter: missing sections[2]');
+}
+const recallEyebrow = requireField(recallSection.eyebrow, 'sections[2].eyebrow');
+const recallCopy = requireField(recallSection.copy, 'sections[2].copy');
+const recallItems = requireField(recallSection.recall, 'sections[2].recall');
+const recallLinks = requireField(recallSection.links, 'sections[2].links');
+// `href` is either an absolute external URL or a base-relative site path.
+const linkHref = (href: string) =>
+ /^https?:/i.test(href) ? href : `${import.meta.env.BASE_URL}${href}`;
---
]+>/g, '')}>
-
← ROUTE MAP
03 / SKILLS
review desk ↗
@@ -55,30 +78,72 @@ const createSteps = requireField(createSection.steps, 'sections[1].steps');
-
-
+ {
+ /* `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. */
+ }
+
+
{anatomyEyebrow.en}
{anatomyCopy.en}
+
+
+
+
-
-
-
+
+
{createEyebrow.en}
+
-
+ ({ label: step.label.en, copy: step.copy.en }))}
+ />
+
+
+
+
+ {recallEyebrow.en}
+
+ {recallCopy.en}
+
+ {
+ recallLinks.map((link) => {
+ const external = /^https?:/i.test(link.href);
+ return (
+
+ {link.label.en}
+
+ );
+ })
+ }
+
+
+
{
- createSteps.map((step, index) => (
-
- {String(index + 1).padStart(2, '0')}
-
- {step.label.en}
- {step.copy.en}
-
-
+ recallItems.map((item) => (
+
+
+ {item.question.en}
+ +
+
+ {item.answer.en}
+
))
}
@@ -91,3 +156,136 @@ const createSteps = requireField(createSection.steps, 'sections[1].steps');
Full guide: skill forge →
+
+
diff --git a/src/pages/summary.astro b/src/pages/summary.astro
index 02615c6..91fb63d 100644
--- a/src/pages/summary.astro
+++ b/src/pages/summary.astro
@@ -18,7 +18,7 @@ const introduction =
{introduction}
-
+
01Models
Capability and effort are separate knobs.
Open chapter →= 2560px) {
+ body {
+ zoom: 1.5;
+ }
+}
+
+@media (width >= 3840px) {
+ body {
+ zoom: 1.75;
+ }
+}
diff --git a/src/styles/motion.css b/src/styles/motion.css
new file mode 100644
index 0000000..c056c1b
--- /dev/null
+++ b/src/styles/motion.css
@@ -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;
+ }
+}
diff --git a/src/styles/tokens.css b/src/styles/tokens.css
index f436f44..f3bfda8 100644
--- a/src/styles/tokens.css
+++ b/src/styles/tokens.css
@@ -121,6 +121,15 @@
--guide-prompt-rule: #ffffff32;
--guide-prompt-enhanced-copy: #e5e3ef;
+ /* Motion. The house curve is the strong ease-out already shipped inline
+ across pages (base.css link transitions, the review desk, the rules
+ island); naming it here lets new motion extend the same curve instead
+ of re-typing near-misses. Durations follow the 150-250ms UI budget in
+ .agents/rules/animation.md. */
+ --ease-out: cubic-bezier(0.2, 0, 0, 1);
+ --dur-press: 160ms;
+ --dur-rise: 200ms;
+
/* Breakpoints */
--bp-sm: 560px;
--bp-md: 800px;