{
  "name": "ST IT Landing Animation Catalog",
  "description": "Каталог анимаций для лендингов: карточки с описанием, кодом и превью. Предназначен для людей и ИИ-агентов, собирающих лендинги.",
  "generated_from": [
    "agent-01-entrance.json",
    "agent-02-text.json",
    "agent-03-scroll.json",
    "agent-04-hover.json",
    "agent-05-nav.json",
    "agent-06-bg.json",
    "agent-07-physics3d.json",
    "agent-08-libs.json"
  ],
  "count": 111,
  "categories": {
    "background": 14,
    "entrance": 14,
    "hover": 15,
    "navigation": 14,
    "physics3d": 13,
    "scroll": 13,
    "technique": 14,
    "text": 14
  },
  "source_domains": [
    "3dtransforms.desandro.com",
    "animejs.com",
    "auto-animate.formkit.com",
    "barba.js.org",
    "blog.karimould.dev",
    "blog.logrocket.com",
    "codepen.io",
    "codeshack.io",
    "css-tricks.com",
    "demos.gsap.com",
    "developer.chrome.com",
    "developer.mozilla.org",
    "easings.net",
    "github.com",
    "gsap.com",
    "help.rive.app",
    "motion.dev",
    "particles.js.org",
    "pudding.cool",
    "r3f.docs.pmnd.rs",
    "scroll-driven-animations.style",
    "splitting.js.org",
    "swiperjs.com",
    "threejs.org",
    "tobiasahlin.com",
    "tympanus.net",
    "web.dev",
    "webdesign.tutsplus.com",
    "webglfundamentals.org",
    "www.dannyguo.com",
    "www.quackit.com",
    "www.smashingmagazine.com",
    "www.vantajs.com",
    "www.w3.org"
  ],
  "cards": [
    {
      "id": "entr-fade-in",
      "name": "Fade In",
      "name_ru": "Плавное проявление",
      "category": "entrance",
      "tags": [
        "on-load",
        "fade",
        "opacity",
        "keyframes"
      ],
      "summary": "Заголовок, подпись и кнопка плавно проявляются из полной прозрачности при загрузке страницы, без какого-либо смещения.",
      "effect": "opacity 0 → 1 через keyframes fade-in, 700 мс ease-out; задержки каскадом: заголовок 0 мс, подпись 120 мс, кнопка 240 мс; элементы не двигаются и не меняют размер.",
      "when_to_use": "Hero и первые экраны, где нужен мягкий старт без движения; уведомления, подсказки, ленивые блоки после загрузки.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <h1 class=\"fade\">Launch faster</h1>\n  <p class=\"fade fade-sub\">Ship your landing page in a weekend, not a quarter.</p>\n  <p class=\"fade fade-note\">No credit card required.</p>\n  <a class=\"fade fade-cta\" href=\"#\">Start free</a>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.demo h1 { margin: 0 0 4px; font-size: 34px; }\n.fade { opacity: 0; animation: fade-in 0.7s ease-out both; }\n.fade-sub { animation-delay: 0.12s; color: #aab6d3; margin: 6px 0 14px; }\n.fade-note { animation-delay: 0.36s; color: #7385a8; font-size: 13px; margin: 12px 0 0; }\n.fade-cta { animation-delay: 0.24s; display: inline-block; padding: 8px 14px; border: 1px solid #3b82f6; border-radius: 8px; color: #8ab4ff; text-decoration: none; font-size: 14px; }\n@keyframes fade-in { to { opacity: 1; } }\n@media (prefers-reduced-motion: reduce) {\n  .fade { animation: none; opacity: 1; }\n}",
        "js": ""
      },
      "source": {
        "name": "MDN — Using CSS animations",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Animations/Using",
        "domain": "developer.mozilla.org"
      },
      "performance": "Анимируется только opacity — работа уходит на композитор, layout и paint не трогаются; безопасно для больших блоков.",
      "a11y": "prefers-reduced-motion: reduce — animation: none и opacity: 1, элементы сразу в конечном состоянии.",
      "browser_support": "CSS animations: Chrome 43+, Safari 9+, Firefox 16+ (Baseline widely available)",
      "ai_use": "On page load, fade hero elements in: opacity 0→1, 700ms ease-out, stagger via animation-delay 120ms between headline, subline and CTA; no movement.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1600,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-fade-in-up",
      "name": "Fade In Up / Directional Slide",
      "name_ru": "Появление со смещением",
      "category": "entrance",
      "tags": [
        "on-load",
        "fade",
        "slide",
        "directional",
        "hero"
      ],
      "summary": "Основной текст въезжает снизу на 26 px одновременно с проявлением, а надзаголовок — слева, задавая направление чтения.",
      "effect": "translateY(26px) + opacity 0 → 0/1, 700 мс cubic-bezier(.2,.7,.2,1); подпись и CTA с задержками 140/280 мс; вариант slide-left — translateX(-30px) для надзаголовка.",
      "when_to_use": "Hero-копирайт и заголовки секций при загрузке; CTA после первого экрана; направление движения задаёт маршрут взгляда.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <p class=\"eyebrow\">New in v2</p>\n  <h1 class=\"headline\">Motion that sells</h1>\n  <p class=\"sub\">Directional entrances guide the eye.</p>\n  <button class=\"cta\">See it live</button>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; color: #f1f5ff; }\n.eyebrow, .headline, .sub, .cta { opacity: 0; animation: rise 0.7s cubic-bezier(.2,.7,.2,1) both; }\n.eyebrow { animation-name: slide-left; color: #8ab4ff; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; margin: 0; }\n.headline { margin: 8px 0; font-size: 34px; }\n.sub { animation-delay: 0.14s; color: #aab6d3; margin: 0; }\n.cta { animation-delay: 0.28s; margin-top: 14px; padding: 8px 14px; border: 1px solid #3b82f6; border-radius: 8px; background: transparent; color: #8ab4ff; }\n@keyframes rise { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }\n@keyframes slide-left { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  .eyebrow, .headline, .sub, .cta { animation: none; opacity: 1; }\n}",
        "js": ""
      },
      "source": {
        "name": "web.dev — How to create high-performance CSS animations",
        "url": "https://web.dev/articles/animations-guide",
        "domain": "web.dev"
      },
      "performance": "transform + opacity — compositor-only; движение short-range (до 30 px), без layout-троггов.",
      "a11y": "prefers-reduced-motion: reduce — анимации отключены, все элементы видимы и статичны.",
      "browser_support": "CSS animations + translate: Chrome 43+, Safari 9+, Firefox 16+ (Baseline widely available)",
      "ai_use": "Apply to hero copy on load: fade + 26px rise, 700ms cubic-bezier(.2,.7,.2,1), subline +140ms, CTA +280ms; use translateX(-30px) variant for a left-entering eyebrow.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1800,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-scale-pop",
      "name": "Scale Pop-In",
      "name_ru": "Появление с масштабированием",
      "category": "entrance",
      "tags": [
        "on-load",
        "scale",
        "pop",
        "card"
      ],
      "summary": "Карточка тарифа «выпрыгивает» из масштаба 0.6 до натурального размера с проявлением.",
      "effect": "opacity 0 + scale(.6) → 1, 500 мс ease-out, без отскока; transform-origin по центру, элемент-карточка выступает как единое целое.",
      "when_to_use": "Карточки тарифов и товаров, бейджи, модальные и popover-блоки, точки на карте.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <div class=\"pop-card\">\n    <span class=\"price\">$29<small>/mo</small></span>\n    <p>Unlimited projects, cancel anytime.</p>\n    <button class=\"cta\">Start free trial</button>\n  </div>\n</section>",
        "css": ".demo { padding: 24px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.pop-card { max-width: 280px; padding: 18px; border: 1px solid #2a3550; border-radius: 14px; animation: pop-in 0.5s ease-out both; }\n.price { font-size: 32px; font-weight: 700; }\n.price small { font-size: 14px; color: #aab6d3; }\n.pop-card p { color: #aab6d3; margin: 6px 0 16px; }\n.cta { padding: 8px 14px; border: 0; border-radius: 8px; background: #3b82f6; color: #fff; }\n@keyframes pop-in { from { opacity: 0; transform: scale(0.6); } }\n@media (prefers-reduced-motion: reduce) {\n  .pop-card { animation: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "MDN — scale() CSS function",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/scale",
        "domain": "developer.mozilla.org"
      },
      "performance": "transform + opacity — compositor-friendly; масштабные анимации держать короткими и по площади не больше карточки.",
      "a11y": "prefers-reduced-motion: reduce — animation: none; MDN отдельно предупреждает, что zoom-эффекты триггерят мигрень, поэтому отключение обязательно.",
      "browser_support": "transform: scale(): Chrome 43+, Safari 9+, Firefox 16+ (Baseline widely available)",
      "ai_use": "Pop a pricing card in on load: scale(.6)→1 with opacity fade, 500ms ease-out, transform-origin center, no overshoot.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1500,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-blur-in",
      "name": "Blur In (Focus Pull)",
      "name_ru": "Появление из размытия",
      "category": "entrance",
      "tags": [
        "on-load",
        "blur",
        "filter",
        "focus"
      ],
      "summary": "Текст выезжает из размытия 10 px и лёгкого увеличения, как наводка камеры на резкость.",
      "effect": "filter blur(10px) + opacity 0 + scale(1.04) → blur(0)/1/1, 900 мс cubic-bezier(.16,.84,.3,1); подпись +180 мс, строка со цифрой +340 мс.",
      "when_to_use": "Hero-заголовки, крупные цифры и статистика, обложки статей — один акцентный текстовый блок на страницу.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <p class=\"kicker\">Now in beta</p>\n  <h1 class=\"focus-title\">Crystal clear analytics</h1>\n  <p class=\"focus-sub\">Track every funnel step in real time.</p>\n  <p class=\"focus-stat\">12,480 events/min</p>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.kicker, .focus-title, .focus-sub, .focus-stat { opacity: 0; animation: focus-in 0.9s cubic-bezier(.16,.84,.3,1) both; }\n.kicker { color: #8ab4ff; font-size: 13px; margin: 0; }\n.focus-title { margin: 8px 0; font-size: 32px; }\n.focus-sub { animation-delay: 0.18s; color: #aab6d3; margin: 0; }\n.focus-stat { animation-delay: 0.34s; margin: 14px 0 0; color: #6ea8ff; font-weight: 600; }\n@keyframes focus-in { from { opacity: 0; filter: blur(10px); transform: scale(1.04); } to { opacity: 1; filter: blur(0); transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  .kicker, .focus-title, .focus-sub, .focus-stat { animation: none; opacity: 1; filter: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "MDN — blur() CSS function",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/filter-function/blur",
        "domain": "developer.mozilla.org"
      },
      "performance": "filter вызывает paint каждый кадр — держать область маленькой (заголовок), не вешать на полноэкранные секции.",
      "a11y": "prefers-reduced-motion: reduce — animation: none и filter: none, текст читается сразу.",
      "browser_support": "filter: blur(): Chrome 53+, Safari 9.1+, Firefox 35+ (Baseline widely available)",
      "ai_use": "Reveal a headline from blur: filter blur(10px)→0 with opacity and scale(1.04)→1, 900ms cubic-bezier(.16,.84,.3,1); stagger following lines 180ms; keep scope to one text block.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-rotate-in",
      "name": "Rotate In",
      "name_ru": "Появление с поворотом",
      "category": "entrance",
      "tags": [
        "on-load",
        "rotate",
        "tilt",
        "badge"
      ],
      "summary": "Бейдж и заголовок въезжают с наклоном −12° и снизу, выравниваясь в финальную позицию.",
      "effect": "rotate(-12deg) + translateY(14px) + opacity 0 → 0, 600 мс cubic-bezier(.2,.7,.2,1); каскад задержек 0/150/300/450 мс; transform-origin: left center для бейджа.",
      "when_to_use": "Бейджи «New/Featured», теги категорий, заголовки фич — акцент, который должен привлечь взгляд.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <span class=\"tag\">✳ Featured</span>\n  <h2>Built for speed</h2>\n  <p>Sub-second loads on every route.</p>\n  <button class=\"cta\">Read the docs</button>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.tag { display: inline-block; padding: 5px 12px; border: 1px solid #3b82f6; border-radius: 999px; color: #8ab4ff; font-size: 13px; transform-origin: left center; opacity: 0; animation: tilt-in 0.6s cubic-bezier(.2,.7,.2,1) both; }\n.demo h2 { margin: 14px 0 4px; font-size: 30px; opacity: 0; animation: tilt-in 0.6s cubic-bezier(.2,.7,.2,1) 0.15s both; }\n.demo p { margin: 0; color: #aab6d3; opacity: 0; animation: tilt-in 0.6s cubic-bezier(.2,.7,.2,1) 0.3s both; }\n.cta { opacity: 0; animation: tilt-in 0.6s cubic-bezier(.2,.7,.2,1) 0.45s both; margin-top: 14px; padding: 8px 14px; border: 0; border-radius: 8px; background: #3b82f6; color: #fff; }\n@keyframes tilt-in { from { opacity: 0; transform: rotate(-12deg) translateY(14px); } to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  .tag, .demo h2, .demo p, .cta { animation: none; opacity: 1; }\n}",
        "js": ""
      },
      "source": {
        "name": "MDN — rotate() CSS function",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/rotate",
        "domain": "developer.mozilla.org"
      },
      "performance": "transform + opacity — compositor-only; небольшой угол (до 15°) не вызывает дискомфорта при короткой длительности.",
      "a11y": "prefers-reduced-motion: reduce — анимации отключены, элементы сразу в финальной позиции.",
      "browser_support": "transform: rotate(): Chrome 43+, Safari 9+, Firefox 16+ (Baseline widely available)",
      "ai_use": "Tilt elements in on load: rotate(-12deg) + 14px rise to 0, 600ms cubic-bezier(.2,.7,.2,1), stagger 150ms per element; set transform-origin: left center on the badge.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-stagger-list",
      "name": "Staggered List Reveal",
      "name_ru": "Поочерёдное появление списка",
      "category": "entrance",
      "tags": [
        "on-scroll",
        "stagger",
        "intersection-observer",
        "transition-delay",
        "list"
      ],
      "summary": "Пункты списка по очереди выезжают снизу, когда список попадает в viewport, читаясь как последовательность.",
      "effect": "IntersectionObserver с threshold 0.3 добавляет класс .is-in; каждый li проходит 14 px вверх и проявляется 500 мс cubic-bezier(.2,.7,.2,1) с transition-delay = индекс × 90 мс; после срабатывания observer отключается.",
      "when_to_use": "Списки шагов процесса, преимущества, чек-листы, этапы работ в секции «как мы это делаем».",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<ul class=\"stagger-list\">\n  <li style=\"--i:0\">Discovery workshop</li>\n  <li style=\"--i:1\">Design sprints</li>\n  <li style=\"--i:2\">Frontend build</li>\n  <li style=\"--i:3\">Launch and support</li>\n</ul>",
        "css": ".stagger-list { margin: 0; padding: 20px; list-style: none; font-family: ui-sans-serif, system-ui, sans-serif; }\n.stagger-list li {\n  margin-bottom: 8px; padding: 12px 14px; border: 1px solid #223055; border-radius: 10px;\n  color: #dbe4ff; opacity: 0; transform: translateY(14px);\n  transition: opacity 0.5s ease, transform 0.5s cubic-bezier(.2,.7,.2,1);\n  transition-delay: calc(var(--i, 0) * 90ms);\n}\n.stagger-list.is-in li { opacity: 1; transform: none; }\n@media (prefers-reduced-motion: reduce) {\n  .stagger-list li { opacity: 1; transform: none; transition: none; }\n}",
        "js": "const list = document.querySelector('.stagger-list');\nconst io = new IntersectionObserver(([entry]) => {\n  if (!entry.isIntersecting) return;\n  list.classList.add('is-in');\n  io.disconnect();\n}, { threshold: 0.3 });\nio.observe(list);"
      },
      "source": {
        "name": "CSS-Tricks — A Handy Little System for Animated Entrances in CSS",
        "url": "https://css-tricks.com/a-handy-little-system-for-animated-entrances-in-css/",
        "domain": "css-tricks.com"
      },
      "performance": "transform + opacity; задержки считает CSS (calc по --i), JS срабатывает один раз и отключает observer.",
      "a11y": "prefers-reduced-motion: reduce — transition: none, все пункты сразу видимы, без каскада.",
      "browser_support": "IntersectionObserver: Chrome 51+, Safari 12.1+, Firefox 55+; CSS custom properties: Baseline",
      "ai_use": "Reveal a list on scroll: IntersectionObserver threshold .3 adds .is-in; li goes opacity 0→1 and translateY(14px)→0 in 500ms with transition-delay index*90ms; disconnect observer after firing.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-stagger-grid",
      "name": "Staggered Grid Reveal (GSAP)",
      "name_ru": "Поочерёдное появление сетки",
      "category": "entrance",
      "tags": [
        "on-load",
        "stagger",
        "grid",
        "gsap"
      ],
      "summary": "Сетка из шести плиток появляется волной слева-вправо: GSAP сам вычисляет строки и колонки для стаггера по сетке.",
      "effect": "gsap.set: opacity 0, y 26, scale .92; затем gsap.to 600 мс power2.out, stagger { each: 80ms, grid: 'auto' } — волна по визуальному расположению плиток.",
      "when_to_use": "Сетки фич и интеграций, логотипы клиентов, галереи кейсов из 4–12 плиток.",
      "tech": [
        "gsap",
        "vanilla-js"
      ],
      "dependencies": [
        "gsap 3.13 (CDN, gsap.min.js)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\"></script>\n<section class=\"demo\">\n  <div class=\"tiles\">\n    <div class=\"tile\">Strategy</div>\n    <div class=\"tile\">Design</div>\n    <div class=\"tile\">Frontend</div>\n    <div class=\"tile\">Motion</div>\n    <div class=\"tile\">Analytics</div>\n    <div class=\"tile\">Support</div>\n  </div>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; }\n.tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n.tile { padding: 20px 8px; text-align: center; border: 1px solid #2a3550; border-radius: 10px; color: #dbe4ff; font-weight: 600; font-size: 13px; }\n@media (prefers-reduced-motion: reduce) {\n  .tile { opacity: 1 !important; transform: none !important; }\n}",
        "js": "const tiles = gsap.utils.toArray('.tile');\nif (matchMedia('(prefers-reduced-motion: reduce)').matches) {\n  gsap.set(tiles, { clearProps: 'all' });\n} else {\n  gsap.set(tiles, { opacity: 0, y: 26, scale: 0.92 });\n  gsap.to(tiles, {\n    opacity: 1, y: 0, scale: 1, duration: 0.6, ease: 'power2.out',\n    stagger: { each: 0.08, grid: 'auto', from: 'start' }\n  });\n}"
      },
      "source": {
        "name": "GSAP Docs — Staggers",
        "url": "https://gsap.com/resources/getting-started/Staggers/",
        "domain": "gsap.com"
      },
      "performance": "GSAP анимирует transform/opacity; grid: 'auto' считает строки/колонки один раз при вызове, для адаптива нужен пересчёт на resize.",
      "a11y": "matchMedia('(prefers-reduced-motion: reduce)') — плитки не скрываются и не анимируются, сразу в финальном состоянии.",
      "browser_support": "GSAP 3.13: все современные браузеры; matchMedia и CSS grid: Baseline widely available",
      "ai_use": "Stagger a feature grid in with GSAP: set tiles opacity:0, y:26, scale:.92, then tween to 1/0/1, duration .6, ease power2.out, stagger { each:.08, grid:'auto', from:'start' }; skip when prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-clip-circle",
      "name": "Circle Clip Reveal",
      "name_ru": "Круговое раскрытие clip-path",
      "category": "entrance",
      "tags": [
        "on-load",
        "clip-path",
        "circle",
        "reveal"
      ],
      "summary": "Промо-панель раскрывается круговой маской из центра до полного размера, проявляясь как из объектива.",
      "effect": "clip-path circle(0% at 50% 55%) → circle(140% at 50% 55%), 900 мс cubic-bezier(.65,0,.35,1), старт через 150 мс; текст внутри не двигается.",
      "when_to_use": "Промо-блоки, карточки офферов, секции с одной ключевой мыслью; фокус внимания в центр блока.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <div class=\"reveal-panel\">\n    <h1>Truly open source</h1>\n    <p>Audit the code, fork it, ship it.</p>\n    <button class=\"cta\">View on GitHub</button>\n  </div>\n</section>",
        "css": ".demo { padding: 24px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.reveal-panel {\n  max-width: 300px; padding: 22px; border: 1px solid #2a3550; border-radius: 14px;\n  animation: circle-reveal 0.9s cubic-bezier(.65,0,.35,1) 0.15s both;\n}\n.reveal-panel h1 { margin: 0; font-size: 28px; }\n.reveal-panel p { color: #aab6d3; margin: 6px 0 16px; }\n.cta { padding: 8px 14px; border: 1px solid #3b82f6; border-radius: 8px; background: transparent; color: #8ab4ff; }\n@keyframes circle-reveal {\n  from { clip-path: circle(0% at 50% 55%); }\n  to { clip-path: circle(140% at 50% 55%); }\n}\n@media (prefers-reduced-motion: reduce) {\n  .reveal-panel { animation: none; clip-path: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "MDN — circle() CSS function",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/basic-shape/circle",
        "domain": "developer.mozilla.org"
      },
      "performance": "clip-path — paint-only свойство: repaint без layout; круги простые, деградации кадров не ожидается.",
      "a11y": "prefers-reduced-motion: reduce — animation: none и clip-path: none, панель сразу видна целиком.",
      "browser_support": "clip-path: circle(): Chrome 55+, Safari 9.1+, Firefox 54+ (Baseline widely available)",
      "ai_use": "Reveal a panel with an expanding circle: clip-path circle(0%→140%) at 50% 55%, 900ms cubic-bezier(.65,0,.35,1), 150ms delay; disable under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1600,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-clip-wipe",
      "name": "Clip-Path Wipe Reveal",
      "name_ru": "Раскрытие-шторка clip-path",
      "category": "entrance",
      "tags": [
        "on-load",
        "clip-path",
        "wipe",
        "inset",
        "curtain"
      ],
      "summary": "Заголовок и подпись открываются слева направо, как раздвигающаяся шторка.",
      "effect": "clip-path inset(0 100% 0 0) → inset(0 0 0 0), 800 мс cubic-bezier(.65,0,.35,1); подпись тем же приёмом с задержкой 250 мс, создавая двухтактный свип.",
      "when_to_use": "Заголовки секций, цитаты, редакционные лендинги — когда нужно ощущение печатного раскрытия текста.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <h1 class=\"wipe\">Design systems that scale</h1>\n  <p class=\"wipe wipe-sub\">Tokens, components and docs in one place.</p>\n</section>",
        "css": ".demo { padding: 20px; max-width: 340px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.demo h1 { margin: 0; font-size: 34px; }\n.wipe { animation: wipe-in 0.8s cubic-bezier(.65,0,.35,1) both; }\n.wipe-sub { margin-top: 8px; color: #aab6d3; animation-delay: 0.25s; }\n@keyframes wipe-in {\n  from { clip-path: inset(0 100% 0 0); }\n  to { clip-path: inset(0 0 0 0); }\n}\n@media (prefers-reduced-motion: reduce) {\n  .wipe { animation: none; clip-path: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks — Animating with Clip-Path",
        "url": "https://css-tricks.com/animating-with-clip-path/",
        "domain": "css-tricks.com"
      },
      "performance": "clip-path — paint-only (repaint без layout); интерполяция inset простых прямоугольников дешёвая.",
      "a11y": "prefers-reduced-motion: reduce — animation: none и clip-path: none; текст присутствует в DOM и доступен скринридерам во время раскрытия.",
      "browser_support": "clip-path: inset(): Chrome 55+, Safari 9.1+, Firefox 54+ (Baseline widely available)",
      "ai_use": "Wipe a headline in from the left: clip-path inset(0 100% 0 0)→inset(0 0 0 0), 800ms cubic-bezier(.65,0,.35,1); repeat for subline with a 250ms delay.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1600,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-mask-text",
      "name": "Mask Text Reveal",
      "name_ru": "Текстовое раскрытие маской",
      "category": "entrance",
      "tags": [
        "on-load",
        "mask",
        "text",
        "gradient",
        "mask-position"
      ],
      "summary": "Заголовок проявляется движущейся градиентной маской слева направо — мягкая волна вместо жёсткой границы.",
      "effect": "mask-image linear-gradient(#000 40%, transparent 60%), mask-size 250% 100%, mask-position 100% → 0 за 1100 мс cubic-bezier(.65,0,.35,1) с задержкой 150 мс; край раскрытия мягкий.",
      "when_to_use": "Крупные hero-заголовки и названия секций, где нужен кинематографичный проявочный проход.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class=\"demo\">\n  <h1 class=\"mask-wipe\">Shipping since 2014</h1>\n  <p>Websites, apps and brands that hold up.</p>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.mask-wipe {\n  margin: 0 0 10px; font-size: 34px; line-height: 1.15;\n  mask-image: linear-gradient(90deg, #000 40%, transparent 60%);\n  mask-size: 250% 100%; mask-repeat: no-repeat; mask-position: 100% 0;\n  animation: mask-sweep 1.1s cubic-bezier(.65,0,.35,1) 0.15s both;\n}\n.demo p { color: #aab6d3; margin: 0; }\n@keyframes mask-sweep { to { mask-position: 0 0; } }\n@media (prefers-reduced-motion: reduce) {\n  .mask-wipe { animation: none; mask-image: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "Codrops — Dynamic CSS Masks with Custom Properties and GSAP",
        "url": "https://tympanus.net/codrops/2021/05/04/dynamic-css-masks-with-custom-properties-and-gsap/",
        "domain": "tympanus.net"
      },
      "performance": "mask-position — paint-only; маска на одном заголовке дешева, но следить за площадью при многострочном тексте.",
      "a11y": "prefers-reduced-motion: reduce — animation: none и mask-image: none; текст всегда в DOM, маска не влияет на a11y-дерево.",
      "browser_support": "mask-image (unprefixed): Chrome 120+, Safari 15.4+, Firefox 53+; в старом Safari нужен -webkit- префикс",
      "ai_use": "Reveal an h1 with a moving gradient mask: mask-image linear-gradient(90deg, #000 40%, transparent 60%), mask-size 250% 100%, animate mask-position 100%→0, 1.1s cubic-bezier(.65,0,.35,1).",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1800,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-svg-draw",
      "name": "SVG Stroke Draw-On",
      "name_ru": "Рисование SVG-линии",
      "category": "entrance",
      "tags": [
        "on-load",
        "svg",
        "stroke",
        "draw",
        "get-total-length"
      ],
      "summary": "SVG-график рисуется линией слева направо, как будто его выводят на экране; подпись появляется после завершения.",
      "effect": "stroke-dasharray и stroke-dashoffset равны длине пути (getTotalLength), offset анимируется до 0 за 1500 мс cubic-bezier(.65,0,.35,1) через 200 мс; подпись fade-up на 1300 мс.",
      "when_to_use": "Схемы и графики, логотипы, иллюстрации в секциях продукта, иконки успеха.",
      "tech": [
        "css",
        "vanilla-js",
        "svg"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class=\"demo\">\n  <svg width=\"240\" height=\"110\" viewBox=\"0 0 240 110\" fill=\"none\" role=\"img\" aria-label=\"Growth chart\">\n    <path class=\"draw\" d=\"M20 90 L70 60 L110 75 L160 30 L220 45\"\n          stroke=\"#6ea8ff\" stroke-width=\"5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n  </svg>\n  <p class=\"caption\">Revenue, last five quarters</p>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.caption { margin: 10px 0 0; color: #aab6d3; opacity: 0; animation: fade-up 0.6s ease 1.3s both; }\n@keyframes fade-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  .caption { animation: none; opacity: 1; }\n}",
        "js": "const path = document.querySelector('.draw');\nconst reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst len = path.getTotalLength();\nif (reduce) {\n  path.style.strokeDashoffset = 0;\n} else {\n  path.style.strokeDasharray = len;\n  path.style.strokeDashoffset = len;\n  path.getBoundingClientRect();\n  path.style.transition = 'stroke-dashoffset 1.5s cubic-bezier(.65,0,.35,1) 0.2s';\n  path.style.strokeDashoffset = '0';\n}"
      },
      "source": {
        "name": "CSS-Tricks — How SVG Line Animation Works",
        "url": "https://css-tricks.com/svg-line-animation-works/",
        "domain": "css-tricks.com"
      },
      "performance": "stroke-dashoffset не композитится (paint каждый кадр), но для 1–2 коротких путей это дешёво; для сложных иллюстраций ограничивать длину.",
      "a11y": "JS проверяет prefers-reduced-motion и сразу ставит offset 0; SVG подписан role=\"img\" и aria-label, caption — обычный текст.",
      "browser_support": "SVG + CSS transitions на stroke: Chrome 43+, Safari 9+, Firefox 16+; getTotalLength: Baseline widely available",
      "ai_use": "Draw an inline SVG path on load: set stroke-dasharray/offset via path.getTotalLength(), transition stroke-dashoffset to 0 in 1.5s cubic-bezier(.65,0,.35,1); skip when prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2400,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-elastic-pop",
      "name": "Elastic Overshoot Pop",
      "name_ru": "Пружинное появление",
      "category": "entrance",
      "tags": [
        "on-load",
        "elastic",
        "overshoot",
        "cubic-bezier",
        "bounce"
      ],
      "summary": "Скидочный бейдж влетает с пружиной и перелётом размера, а кнопка — через easeOutBack cubic-bezier.",
      "effect": "keyframes bounce-in: scale .3 → 1.15 (55%) → .94 (75%) → 1 за 900 мс; кнопка — back-in с cubic-bezier(.34,1.56,.64,1) (easeOutBack) 700 мс и задержкой 240 мс.",
      "when_to_use": "Промо и распродажи, ключевые CTA, бейджи скидок — места, где уместна игривая упругость.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <span class=\"burst\">−40%</span>\n  <h2>Black Friday</h2>\n  <p>Annual plans, one weekend only.</p>\n  <button class=\"cta\">Claim offer</button>\n</section>",
        "css": ".demo { padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; }\n.burst { display: inline-block; padding: 6px 14px; border-radius: 999px; background: #3b82f6; color: #fff; font-weight: 700; opacity: 0; animation: bounce-in 0.9s both; }\n.demo h2 { margin: 12px 0 4px; font-size: 30px; opacity: 0; animation: bounce-in 0.9s 0.12s both; }\n.demo p { color: #aab6d3; margin: 0 0 14px; }\n.cta { opacity: 0; animation: back-in 0.7s cubic-bezier(.34,1.56,.64,1) 0.24s both; padding: 8px 16px; border: 0; border-radius: 8px; background: #3b82f6; color: #fff; }\n@keyframes bounce-in {\n  0% { opacity: 0; transform: scale(0.3); }\n  55% { opacity: 1; transform: scale(1.15); }\n  75% { transform: scale(0.94); }\n  100% { opacity: 1; transform: scale(1); }\n}\n@keyframes back-in { from { opacity: 0; transform: scale(0.4); } to { opacity: 1; transform: scale(1); } }\n@media (prefers-reduced-motion: reduce) {\n  .burst, .demo h2, .cta { animation: none; opacity: 1; }\n}",
        "js": ""
      },
      "source": {
        "name": "Easing Functions Cheat Sheet — easings.net (easeOutBack)",
        "url": "https://easings.net/",
        "domain": "easings.net"
      },
      "performance": "transform + opacity — compositor-only; перелёт масштаба не трогает layout, безопасно даже на нескольких элементах.",
      "a11y": "prefers-reduced-motion: reduce — все анимации отключены, элементы сразу в конечном состоянии, без пружины.",
      "browser_support": "CSS keyframes + cubic-bezier: Chrome 43+, Safari 9+, Firefox 16+ (Baseline widely available)",
      "ai_use": "Pop a promo badge in with overshoot keyframes (scale .3→1.15→.94→1, 900ms) and use transition/tween with cubic-bezier(.34,1.56,.64,1) for the CTA; disable under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1800,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-preloader-counter",
      "name": "Counter Preloader Reveal",
      "name_ru": "Прелоадер со счётчиком",
      "category": "entrance",
      "tags": [
        "on-load",
        "preloader",
        "counter",
        "progress",
        "reveal"
      ],
      "summary": "Экран прелоадера отсчитывает 0–100%, затем уезжает вверх и открывает контент страницы.",
      "effect": "requestAnimationFrame 1500 мс с easeOutCubic: счётчик процентов и ширина полосы; по завершении класс .done уводит прелоадер translateY(-100%) за 600 мс, контент поднимается на 16 px и проявляется с задержкой 450 мс.",
      "when_to_use": "Hero-страницы с тяжёлым видео или ассетами, портфолио и промо-страницы, где нужен срежиссированный вход.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"preloader\" id=\"preloader\">\n  <div class=\"counter\" id=\"counter\">0%</div>\n  <div class=\"loader-bar\"><span id=\"bar\"></span></div>\n</div>\n<section class=\"page\" id=\"page\">\n  <h1>Studio Nord</h1>\n  <p>We design calm interfaces for busy teams.</p>\n</section>",
        "css": ".preloader {\n  display: none; position: fixed; inset: 0; z-index: 10; place-content: center; gap: 16px;\n  background: #0b0d12; color: #eef2ff; font: 700 40px/1 ui-sans-serif, system-ui, sans-serif;\n  transition: transform 0.6s cubic-bezier(.65,0,.35,1), opacity 0.4s 0.3s;\n}\n.js-ready .preloader { display: grid; }\n.preloader.done { transform: translateY(-100%); opacity: 0; pointer-events: none; }\n.counter { text-align: center; font-variant-numeric: tabular-nums; }\n.loader-bar { width: 180px; height: 4px; border-radius: 999px; background: #1c2333; overflow: hidden; }\n.loader-bar span { display: block; width: 0%; height: 100%; background: #6ea8ff; }\n.page { padding: 24px; font-family: ui-sans-serif, system-ui, sans-serif; color: #eef2ff; transition: opacity 0.6s ease 0.45s, transform 0.6s ease 0.45s; }\n.js-ready .page { opacity: 0; transform: translateY(16px); }\n.page.show { opacity: 1; transform: none; }\n@media (prefers-reduced-motion: reduce) {\n  .preloader, .page { transition: none; }\n}",
        "js": "document.documentElement.classList.add('js-ready');\nconst reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst preloader = document.getElementById('preloader');\nconst counter = document.getElementById('counter');\nconst bar = document.getElementById('bar');\nconst page = document.getElementById('page');\nconst DURATION = 1500;\n\nfunction finish() {\n  preloader.classList.add('done');\n  page.classList.add('show');\n  setTimeout(() => { preloader.style.display = 'none'; }, 700);\n}\n\nfunction tick(now, start) {\n  const p = Math.min((now - start) / DURATION, 1);\n  const eased = 1 - Math.pow(1 - p, 3);\n  counter.textContent = Math.round(eased * 100) + '%';\n  bar.style.width = (eased * 100) + '%';\n  if (p < 1) requestAnimationFrame((t) => tick(t, start));\n  else setTimeout(finish, 200);\n}\n\nif (reduce) {\n  counter.textContent = '100%';\n  bar.style.width = '100%';\n  finish();\n} else {\n  requestAnimationFrame((t) => tick(t, t));\n}"
      },
      "source": {
        "name": "Codrops — Page Preloading Effect",
        "url": "https://tympanus.net/codrops/2014/08/05/page-preloading-effect/",
        "domain": "tympanus.net"
      },
      "performance": "Счётчик трогает textContent и width полосы (layout у 4px-элемента) — дёшево; прелоадер fixed, не влияет на layout страницы.",
      "a11y": "reduce — счётчик сразу 100%, прелоадер мгновенно скрывается, контент виден сразу; без JS контент виден по умолчанию (стили скрытия под .js-ready).",
      "browser_support": "requestAnimationFrame и transforms: Chrome 24+, Safari 7+, Firefox 23+ (Baseline widely available)",
      "ai_use": "Add a ~1.5s counter preloader: rAF with ease-out counting 0–100% (text + bar width), then slide the preloader up (-100%) over 600ms and fade page content in; bypass entirely when prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 3200,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "entr-batch-reveal",
      "name": "Batch Scroll Reveal Utility",
      "name_ru": "Пакетное появление по data-атрибуту",
      "category": "entrance",
      "tags": [
        "on-scroll",
        "intersection-observer",
        "data-attribute",
        "utility",
        "stagger"
      ],
      "summary": "Одна утилита на всю страницу: элементы с data-reveal появляются при входе в viewport, варианты up/left/zoom и задержка настраиваются атрибутами.",
      "effect": "IntersectionObserver threshold 0.2 добавляет .is-revealed один раз (unobserve после срабатывания); transition 600 мс cubic-bezier(.2,.7,.2,1), задержка из --reveal-delay, которую JS берёт из data-reveal-delay; без JS элементы видимы, стили скрытия под .js-ready.",
      "when_to_use": "Все секции лендинга разом: фичи, кейсы, шаги, футерные блоки — один скрипт и разметка атрибутами.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class=\"demo\">\n  <div class=\"card\" data-reveal=\"up\">Strategy</div>\n  <div class=\"card\" data-reveal=\"left\" data-reveal-delay=\"120\">Design</div>\n  <div class=\"card\" data-reveal=\"zoom\" data-reveal-delay=\"240\">Build</div>\n</section>",
        "css": ".demo { display: grid; gap: 12px; padding: 20px; font-family: ui-sans-serif, system-ui, sans-serif; }\n.card { padding: 18px; border: 1px solid #2a3550; border-radius: 10px; color: #dbe4ff; }\n.js-ready [data-reveal] {\n  opacity: 0;\n  transition: opacity 0.6s ease, transform 0.6s cubic-bezier(.2,.7,.2,1);\n  transition-delay: var(--reveal-delay, 0ms);\n}\n.js-ready [data-reveal=\"up\"] { transform: translateY(24px); }\n.js-ready [data-reveal=\"left\"] { transform: translateX(-28px); }\n.js-ready [data-reveal=\"zoom\"] { transform: scale(0.9); }\n.js-ready [data-reveal].is-revealed { opacity: 1; transform: none; }\n@media (prefers-reduced-motion: reduce) {\n  .js-ready [data-reveal] { opacity: 1; transform: none; transition: none; }\n}",
        "js": "document.documentElement.classList.add('js-ready');\nconst els = document.querySelectorAll('[data-reveal]');\nconst io = new IntersectionObserver((entries) => {\n  entries.forEach((entry) => {\n    if (!entry.isIntersecting) return;\n    entry.target.classList.add('is-revealed');\n    io.unobserve(entry.target);\n  });\n}, { threshold: 0.2 });\n\nels.forEach((el) => {\n  if (el.dataset.revealDelay) el.style.setProperty('--reveal-delay', el.dataset.revealDelay + 'ms');\n  io.observe(el);\n});"
      },
      "source": {
        "name": "MDN — Intersection Observer API",
        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API",
        "domain": "developer.mozilla.org"
      },
      "performance": "transform + opacity — compositor-friendly; один observer на все элементы, unobserve после раскрытия, никаких scroll-листенеров.",
      "a11y": "prefers-reduced-motion: reduce — переходы отключены, элементы сразу видимы; без JS контент тоже виден (скрытие только под .js-ready).",
      "browser_support": "IntersectionObserver: Chrome 51+, Safari 12.1+, Firefox 55+ (Baseline widely available)",
      "ai_use": "Add a page-wide scroll-reveal utility: observe [data-reveal], add .is-revealed on intersect (threshold .2, unobserve after); variants via data-reveal=\"up|left|zoom\", delay via data-reveal-delay (px custom property --reveal-delay); gate hidden styles behind html.js-ready.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-01-entrance.json"
    },
    {
      "id": "text-typewriter-js",
      "name": "Typewriter With Blinking Caret",
      "name_ru": "Печатная машинка с мигающим курсором",
      "category": "text",
      "tags": [
        "typewriter",
        "caret",
        "typing",
        "loop",
        "hero"
      ],
      "summary": "Строка печатается по символу, мигающий курсор остаётся в конце, затем текст стирается и печатается следующая фраза — бесконечный цикл.",
      "effect": "JS добавляет по одному символу каждые 90 мс, после паузы 1.4 с стирает по 45 мс на символ и берёт следующую из трёх фраз; между фразами пауза 350 мс. Курсор — блок 0.6ch, мигает 1 с через opacity со steps(1).",
      "when_to_use": "Hero-заголовок лендинга, промо-строка над сгибом, терминальная секция — когда нужно удержать внимание на короткой меняющейся фразе.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <p class=\"typeline\" aria-live=\"polite\">\n    <span class=\"typetext\"></span><span class=\"caret\" aria-hidden=\"true\"></span>\n  </p>\n  <p class=\"sub\">Deploy previews on every push.</p>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; gap: 10px;\n  padding: 24px; background: #0b0d12; color: #e8ecf4;\n  font: 600 clamp(22px, 6vw, 34px)/1.3 ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n.sub { font: 400 14px/1.4 system-ui, sans-serif; color: #8b93a7; }\n.caret {\n  display: inline-block; width: 0.6ch; height: 1.1em; margin-left: 2px;\n  vertical-align: -0.15em; background: #7dd3fc;\n  animation: blink 1s steps(1) infinite;\n}\n@keyframes blink { 50% { opacity: 0; } }\n@media (prefers-reduced-motion: reduce) { .caret { animation: none; } }",
        "js": "const el = document.querySelector('.typetext');\nconst lines = ['Ship faster.', 'Scale calmly.', 'Deploy daily.'];\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nlet li = 0, ci = 0, deleting = false;\nel.textContent = lines[0];\nfunction tick() {\n  const cur = lines[li];\n  ci += deleting ? -1 : 1;\n  el.textContent = cur.slice(0, ci);\n  let wait = deleting ? 45 : 90;\n  if (!deleting && ci === cur.length) { wait = 1400; deleting = true; }\n  else if (deleting && ci === 0) { deleting = false; li = (li + 1) % lines.length; wait = 350; }\n  setTimeout(tick, wait);\n}\nif (!reduced) tick();"
      },
      "source": {
        "name": "CSS-Tricks — Typewriter Animation That Handles Anything You Throw at It",
        "url": "https://css-tricks.com/typewriter-animation-that-handles-anything-you-throw-at-it/",
        "domain": "css-tricks.com"
      },
      "performance": "Переписывается только textContent каждые 45–90 мс — дёшево; мигание курсора — opacity (compositor). Не запускать много таких блоков на одной странице.",
      "a11y": "prefers-reduced-motion гасит набор и мигание (проверка в JS + CSS-однострочник), остаётся статичная первая строка; смена фраз объявляется через aria-live=\"polite\".",
      "browser_support": "Chrome 88+, Safari 14+, Firefox 87+ (matchMedia, clamp).",
      "ai_use": "On hero load, type phrases char-by-char (90ms/char), hold 1.4s, delete at 45ms/char, loop; show a 0.6ch block caret blinking 1s steps(1); disable typing and blink under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-typewriter-css-steps",
      "name": "Pure CSS Typewriter (steps())",
      "name_ru": "CSS-печатная машинка на steps()",
      "category": "text",
      "tags": [
        "typewriter",
        "steps",
        "css-only",
        "monospace",
        "caret"
      ],
      "summary": "Чистый CSS: строка набирается дискретными шагами steps(18) по моноширинному шрифту, курсор мигает, текст стирается и цикл повторяется.",
      "effect": "max-width 0 → 18ch снапится через steps(18) за 1.8 с (0–45% цикла 4 с), пауза до 75%, обратный проход steps(18) к 0 к 100%. Курсор — border-right, мигает step-end 1 с.",
      "when_to_use": "Секция с кодом, developer-tool лендинги, терминальная строка в hero; только для коротких однострочников на monospace.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <p class=\"term\"><span class=\"cmd\">npx create-landing</span></p>\n  <p class=\"hint\">Pure CSS — width 0 → 18ch snapped by steps(18)</p>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; gap: 12px;\n  padding: 24px; background: #0b0d12; color: #d1fae5;\n  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n.cmd {\n  display: inline-block; overflow: hidden; white-space: nowrap;\n  vertical-align: bottom; padding-right: 2px;\n  border-right: 0.55ch solid #34d399;\n  animation: typing 4s infinite, caret 1s step-end infinite;\n}\n.hint { font: 400 13px/1.4 ui-monospace, monospace; color: #6b7f8e; }\n@keyframes typing {\n  0% { max-width: 0; animation-timing-function: steps(18); }\n  45%, 75% { max-width: 18ch; animation-timing-function: steps(18); }\n  100% { max-width: 0; }\n}\n@keyframes caret { 50% { border-color: transparent; } }\n@media (prefers-reduced-motion: reduce) {\n  .cmd { animation: none; max-width: none; border-right-color: transparent; }\n}",
        "js": "/* No JavaScript required — width is animated in CSS with steps(). */"
      },
      "source": {
        "name": "CSS-Tricks — Typewriter Effect (CSS snippet)",
        "url": "https://css-tricks.com/snippets/css/typewriter-effect/",
        "domain": "css-tricks.com"
      },
      "performance": "Анимируется max-width (layout) у одного короткого inline-block — 60 fps держит; тиражировать эффект по странице не стоит.",
      "a11y": "prefers-reduced-motion: animation: none, полный текст сразу, каретка не мигает; текст в DOM целиком — скринридер читает без ожидания.",
      "browser_support": "Chrome 26+, Safari 7+, Firefox 16+ (steps(), ch-единицы).",
      "ai_use": "For a terminal-style hero line: animate width 0→18ch with steps(18) on a monospace element (hold mid-cycle, then erase), blinking border-right caret; disable all animation and show full text under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-split-chars-stagger",
      "name": "Split Text by Chars with Stagger",
      "name_ru": "Разбивка по буквам со стаггером",
      "category": "text",
      "tags": [
        "text-split",
        "chars",
        "stagger",
        "vanilla-js",
        "loop"
      ],
      "summary": "JS разбивает заголовок на отдельные буквы; каждая появляется со сдвигом 40 мс — снизу вверх с лёгким поворотом; цикл повторяется каждые 3.4 с.",
      "effect": "Буквы: opacity 0→1, translateY 0.6em→0 и rotate 4deg→0 за 620 мс cubic-bezier(.2,.7,.2,1), задержка calc(var(--i) * 40ms). Реплей — снятие и навешивание класса .play с принудительным рефлоу.",
      "when_to_use": "Заголовок hero на загрузке страницы, заголовки секций и офферов длиной 10–30 символов.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo\">\n  <h2 class=\"split\" aria-label=\"Motion that sells.\">Motion that sells.</h2>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center;\n  padding: 24px; background: #0b0d12; color: #f4f7ff;\n  font: 700 clamp(28px, 8vw, 44px)/1.15 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  letter-spacing: -0.02em;\n}\n.ch { display: inline-block; opacity: 0; transform: translateY(0.6em) rotate(4deg); }\n.play .ch {\n  animation: char-in 620ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards;\n  animation-delay: calc(var(--i) * 40ms);\n}\n@keyframes char-in { to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  .ch { opacity: 1; transform: none; animation: none !important; }\n}",
        "js": "const h = document.querySelector('.split');\nconst text = h.textContent;\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nh.innerHTML = [...text].map((c, i) =>\n  `<span class=\"ch\" aria-hidden=\"true\" style=\"--i:${i}\">${c === ' ' ? '&nbsp;' : c}</span>`\n).join('');\nif (!reduced) {\n  const play = () => { h.classList.remove('play'); void h.offsetWidth; h.classList.add('play'); };\n  play();\n  setInterval(play, 3400);\n}"
      },
      "source": {
        "name": "Splitting.js — CSS variables for split words & chars",
        "url": "https://splitting.js.org/",
        "domain": "splitting.js.org"
      },
      "performance": "Только opacity/transform — compositor-friendly; десятки span'ов добавляют DOM-узлы, не применять к длинным абзацам.",
      "a11y": "Буквы обёрнуты в span с aria-hidden, у заголовка aria-label с полным текстом; при prefers-reduced-motion реплей не запускается и текст виден сразу.",
      "browser_support": "Chrome 85+, Safari 14+, Firefox 80+ (CSS custom properties в calc(), Array spread по строке).",
      "ai_use": "Split a short heading into per-char spans (aria-label on heading, aria-hidden letters) and stagger each 620ms fade/rise-in by 40ms; replay every ~3.4s; keep static text under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1700,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-split-words-stagger",
      "name": "Split Text by Words with Blur Stagger",
      "name_ru": "Разбивка по словам со стаггером и блюром",
      "category": "text",
      "tags": [
        "text-split",
        "words",
        "blur",
        "stagger",
        "loop"
      ],
      "summary": "Заголовок делится на слова, каждое проявляется из размытия: blur(10px) → 0 с подъёмом снизу, стаггер 90 мс; цикл повторяется каждые 3.6 с.",
      "effect": "Слова: opacity 0→1, filter blur(10px)→0, translateY 0.45em→0 за 700 мс cubic-bezier(.2,.7,.2,1), задержка calc(var(--i) * 90ms).",
      "when_to_use": "Hero-заголовки в 3–6 слов, подзаголовки фич, крупные фразы соцдоказательства.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo\">\n  <h2 class=\"split\" aria-label=\"Interfaces that feel alive\">Interfaces that feel alive</h2>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center;\n  padding: 24px; background: #0b0d12; color: #eef2ff;\n  font: 800 clamp(26px, 7.5vw, 40px)/1.2 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  letter-spacing: -0.02em;\n}\n.wd { display: inline-block; opacity: 0; filter: blur(10px); transform: translateY(0.45em); }\n.play .wd {\n  animation: word-in 700ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards;\n  animation-delay: calc(var(--i) * 90ms);\n}\n@keyframes word-in { to { opacity: 1; filter: blur(0); transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  .wd { opacity: 1; filter: none; transform: none; animation: none !important; }\n}",
        "js": "const h = document.querySelector('.split');\nconst words = h.textContent.trim().split(/\\s+/);\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nh.innerHTML = words.map((w, i) =>\n  `<span class=\"wd\" aria-hidden=\"true\" style=\"--i:${i}\">${w}</span>`\n).join(' ');\nif (!reduced) {\n  const play = () => { h.classList.remove('play'); void h.offsetWidth; h.classList.add('play'); };\n  play();\n  setInterval(play, 3600);\n}"
      },
      "source": {
        "name": "GSAP — SplitText plugin docs (split by words)",
        "url": "https://gsap.com/docs/v3/Plugins/SplitText/",
        "domain": "gsap.com"
      },
      "performance": "filter: blur() перерисовывает слой каждый кадр — дороже transform-only; ограничить 3–6 словами.",
      "a11y": "aria-label на заголовке, слова aria-hidden; prefers-reduced-motion отключает blur-анимацию, текст сразу читаем.",
      "browser_support": "Chrome 85+, Safari 14+, Firefox 80+ (animate filter, custom properties в delay).",
      "ai_use": "Split a heading into word spans and animate blur(10px)+rise+opacity in 700ms with 90ms per-word stagger; replay every 3.6s; no filter/animation under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1800,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-split-lines-mask-reveal",
      "name": "Split Lines Mask Reveal",
      "name_ru": "Построчный выезд из-под маски",
      "category": "text",
      "tags": [
        "text-split",
        "lines",
        "mask",
        "gsap",
        "stagger"
      ],
      "summary": "GSAP SplitText режет заголовок на строки и прячет их под маски; строки выезжают снизу yPercent 120→0 со стаггером 120 мс, цикл повторяется.",
      "effect": "SplitText.create(type:'lines', mask:'lines') оборачивает строки в overflow:clip; gsap timeline.from(lines, yPercent:120, 0.9 с power4.out, stagger 0.12), repeat -1 с паузой 1.8 с.",
      "when_to_use": "Крупный hero-заголовок в 2–3 строки, заголовок секции — когда нужен «шторный» выезд строк.",
      "tech": [
        "gsap",
        "css"
      ],
      "dependencies": [
        "gsap 3.13.0 (CDN)",
        "SplitText 3.13.0 (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js\"></script>\n<div class=\"demo\">\n  <h2 class=\"split\">Bold words, masked lines, zero jank.</h2>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; max-width: 420px;\n  margin: 0 auto; padding: 24px; background: #0b0d12; color: #f8fafc;\n  font: 800 clamp(28px, 8vw, 42px)/1.15 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  letter-spacing: -0.02em;\n}\n.split { margin: 0; }\n@media (prefers-reduced-motion: reduce) {\n  .split .line { transform: none !important; }\n}",
        "js": "const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nif (!reduced && window.SplitText) {\n  document.fonts.ready.then(() => {\n    const split = SplitText.create('.split', { type: 'lines', mask: 'lines' });\n    gsap.timeline({ repeat: -1, repeatDelay: 1.8 })\n      .from(split.lines, { yPercent: 120, duration: 0.9, ease: 'power4.out', stagger: 0.12 });\n  });\n}"
      },
      "source": {
        "name": "GSAP Demo Hub — SplitText",
        "url": "https://demos.gsap.com/plugin/splittext/",
        "domain": "demos.gsap.com"
      },
      "performance": "yPercent на строках — только compositor; маски — overflow: clip без reflow; при частых ресайзах вызывать split.revert().",
      "a11y": "SplitText сам ставит aria-label родителю и aria-hidden строкам; при prefers-reduced-motion анимация не создаётся — обычный статичный заголовок.",
      "browser_support": "Chrome 115+, Safari 16.4+, Firefox 110+ (GSAP 3.13 + overflow: clip).",
      "ai_use": "Use GSAP SplitText (type:'lines', mask:'lines') on an h2, then timeline.from(lines,{yPercent:120,duration:.9,ease:'power4.out',stagger:.12}) looping with repeatDelay 1.8s; skip entirely under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2400,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-scramble-decode",
      "name": "Text Scramble / Decode",
      "name_ru": "Скрамбл-декодирование текста",
      "category": "text",
      "tags": [
        "scramble",
        "decode",
        "monospace",
        "text-effect",
        "loop"
      ],
      "summary": "Строка «декодируется»: слева направо открываются настоящие символы, а ещё не открытые места каждые ~28 мс перебирают случайные глифы; пауза и повтор.",
      "effect": "Позиция раскрытия растёт на 0.7 символа за тик 28 мс; закрытые позиции заполняются случайным символом набора; когда всё открыто — пауза ~2 с и цикл заново.",
      "when_to_use": "Терминальная секция, developer-tool лендинг, статус-лейбл запуска продукта, техно-стилистика.",
      "tech": [
        "vanilla-js",
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo\">\n  <p class=\"scramble\" data-text=\"PRODUCT LAUNCH READY\">PRODUCT LAUNCH READY</p>\n  <p class=\"hint\">decode loop every ~3s</p>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; gap: 10px;\n  padding: 24px; background: #0b0d12; color: #7dd3fc;\n  font: 600 clamp(20px, 5.5vw, 30px)/1.3 ui-monospace, SFMono-Regular, Menlo, monospace;\n  letter-spacing: 0.06em;\n}\n.hint { font: 400 13px/1.4 ui-monospace, monospace; color: #64748b; letter-spacing: 0; }\n@media (prefers-reduced-motion: reduce) {\n  .scramble { animation: none !important; transition: none !important; }\n}",
        "js": "const el = document.querySelector('.scramble');\nconst target = el.dataset.text;\nconst charset = '!<>-_/[]{}—=+*^?#';\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nif (!reduced) {\n  const run = () => {\n    let pos = 0;\n    const timer = setInterval(() => {\n      pos += 0.7;\n      el.textContent = [...target].map((ch, i) =>\n        ch === ' ' ? ' ' : i < pos ? ch : charset[(Math.random() * charset.length) | 0]\n      ).join('');\n      if (pos >= target.length) { clearInterval(timer); el.textContent = target; }\n    }, 28);\n  };\n  run();\n  setInterval(run, 3400);\n}"
      },
      "source": {
        "name": "Anime.js — scrambleText documentation",
        "url": "https://animejs.com/documentation/text/scrambletext",
        "domain": "animejs.com"
      },
      "performance": "Один таймер 28 мс и перезапись textContent — минимум нагрузки; для длинных строк снизить частоту тиков.",
      "a11y": "При prefers-reduced-motion скрамбл не запускается, показывается финальный текст; исходный текст присутствует в разметке и HTML.",
      "browser_support": "Chrome 88+, Safari 14+, Firefox 87+ (spread строки, matchMedia, dataset).",
      "ai_use": "Scramble a monospace caption: reveal left-to-right while unrevealed chars cycle random glyphs every ~28ms; hold ~2s and loop; render final text only under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1500,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-gradient-shine-sweep",
      "name": "Gradient Shine Sweep",
      "name_ru": "Световой блик по заголовку",
      "category": "text",
      "tags": [
        "gradient",
        "shine",
        "background-clip",
        "loop",
        "hero"
      ],
      "summary": "По заголовку пробегает светлый блик: градиентная заливка текста с узкой белой полосой смещается слева направо, бесшовно зациклено.",
      "effect": "background-size 200%, background-position 0% → 200% linear 2.8 с infinite — сдвиг ровно на полную ширину картинки даёт бесшовный цикл; блик — белая полоса на 40–60% градиента.",
      "when_to_use": "Hero-заголовок или ключевая цифра на тёмном фоне; короткая фраза 2–4 слова, премиальный акцент.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <h1 class=\"shine\">Launch week is here</h1>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center;\n  padding: 24px; background: #0b0d12;\n}\n.shine {\n  margin: 0;\n  font: 800 clamp(30px, 9vw, 48px)/1.15 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  letter-spacing: -0.02em;\n  background: linear-gradient(110deg, #64748b 0%, #94a3b8 40%, #ffffff 50%, #94a3b8 60%, #64748b 100%);\n  background-size: 200% auto;\n  -webkit-background-clip: text; background-clip: text;\n  -webkit-text-fill-color: transparent; color: transparent;\n  animation: sweep 2.8s linear infinite;\n}\n@keyframes sweep {\n  from { background-position: 0% center; }\n  to { background-position: 200% center; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .shine { animation: none; background-position: 50% center; }\n}",
        "js": "/* No JavaScript required — gradient underline sweep is CSS-only. */"
      },
      "source": {
        "name": "CodeShack — Gradient Text Shine Effect CSS",
        "url": "https://codeshack.io/gradient-text-shine-effect-css/",
        "domain": "codeshack.io"
      },
      "performance": "background-position — paint-bound, не compositor; один заголовок ок, на больших блоках текста не применять.",
      "a11y": "prefers-reduced-motion останавливает свип и фиксирует блик по центру; текст остаётся читаемым, контраст светлого блика на тёмном фоне достаточный.",
      "browser_support": "background-clip: text без префикса — Chrome/Edge 120+, Safari 15.5+, Firefox 49+ (с префиксом шире).",
      "ai_use": "Sweep a white highlight across a heading: background-clip:text with a 40/50/60% light band, background-size:200%, animate background-position 0%→200% over 2.8s linear infinite; freeze centered under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-variable-weight-hover",
      "name": "Variable Font Weight Morph",
      "name_ru": "Морфинг начертания на variable font",
      "category": "text",
      "tags": [
        "variable-font",
        "hover",
        "weight",
        "font-variation-settings",
        "morph"
      ],
      "summary": "Начертание заголовка плавно перетекает с 250 до 900 при наведении или фокусе через font-variation-settings 'wght'.",
      "effect": "font-variation-settings 'wght' 250 → 900 плюс fallback font-weight, 600 мс cubic-bezier(.2,.7,.2,1); letter-spacing поджимается с 0.01em до -0.03em.",
      "when_to_use": "Крупный заголовок hero, логотип-надпись, интерактивный акцент в фичах — там, где есть variable font или system-ui.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <p class=\"morph\" tabindex=\"0\">Weightless</p>\n  <p class=\"hint\">hover: wght 250 → 900</p>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; gap: 8px;\n  padding: 24px; background: #0b0d12; color: #fafafa; text-align: center;\n}\n.morph {\n  margin: 0;\n  font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  font-size: clamp(36px, 12vw, 60px);\n  font-weight: 250;\n  font-variation-settings: \"wght\" 250;\n  letter-spacing: 0.01em;\n  cursor: default;\n  transition: font-variation-settings 0.6s cubic-bezier(0.2, 0.7, 0.2, 1),\n              font-weight 0.6s cubic-bezier(0.2, 0.7, 0.2, 1),\n              letter-spacing 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n.morph:hover, .morph:focus-visible {\n  font-weight: 900;\n  font-variation-settings: \"wght\" 900;\n  letter-spacing: -0.03em;\n}\n.hint { font: 400 13px/1.4 system-ui, sans-serif; color: #8b93a7; }\n@media (prefers-reduced-motion: reduce) {\n  .morph { transition-duration: 0.01s; }\n}",
        "js": "/* No JavaScript required — pure CSS transition on font variation axis. */"
      },
      "source": {
        "name": "MDN — font-variation-settings",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-variation-settings",
        "domain": "developer.mozilla.org"
      },
      "performance": "Интерполяция осей шрифта перерисовывает глифы каждый кадр (paint + метрики); допустимо для одного короткого заголовка.",
      "a11y": "prefers-reduced-motion сокращает transition до 0.01 s (мгновенная смена); :focus-visible даёт тот же морф с клавиатуры.",
      "browser_support": "font-variation-settings: Chrome 62+, Safari 11+, Firefox 62+; ось 'wght' анимируется при наличии вариативного шрифта (SF Pro, Segoe UI Variable), иначе отработает font-weight-fallback.",
      "ai_use": "On hover/focus of a display word, morph font-variation-settings 'wght' 250→900 and font-weight fallback over 600ms cubic-bezier(.2,.7,.2,1) while tightening letter-spacing; near-instant under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": ".morph",
        "wait_ms": 1800,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-marquee-ticker",
      "name": "Infinite Marquee Ticker for Headings",
      "name_ru": "Бесконечная бегущая строка-тикер",
      "category": "text",
      "tags": [
        "marquee",
        "ticker",
        "infinite",
        "headline",
        "css-only"
      ],
      "summary": "Бесконечная бегущая строка с заголовками: трек продублирован и едет влево на -50% без шва; края растворяются маской, hover ставит ленту на паузу.",
      "effect": "transform translateX 0 → -50% linear 16 с infinite; трек — две идентичные группы с padding-right = gap, поэтому -50% попадает точно на стык. mask-image даёт мягкие края, hover/focus-within — пауза.",
      "when_to_use": "Лента ценностей/УТП под hero, строка бейджей, новостной тикер на посадочной.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <div class=\"marquee\" aria-label=\"Release highlights\">\n    <div class=\"track\">\n      <span class=\"group\">SHIP FASTER<i>•</i>DESIGN LOUDER<i>•</i>MEASURE EVERYTHING<i>•</i></span>\n      <span class=\"group\" aria-hidden=\"true\">SHIP FASTER<i>•</i>DESIGN LOUDER<i>•</i>MEASURE EVERYTHING<i>•</i></span>\n    </div>\n  </div>\n  <p class=\"hint\">hover the ribbon to pause</p>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; gap: 14px;\n  background: #0b0d12; padding: 24px 0; overflow: hidden;\n}\n.marquee {\n  overflow: hidden;\n  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);\n  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);\n}\n.track {\n  display: flex; width: max-content; white-space: nowrap;\n  font: 800 clamp(24px, 7vw, 36px)/1.1 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  color: #eef2ff; letter-spacing: 0.02em;\n  animation: ticker 16s linear infinite;\n}\n.group { display: inline-flex; gap: 28px; padding-right: 28px; }\n.group i { color: #38bdf8; font-style: normal; }\n@keyframes ticker { to { transform: translateX(-50%); } }\n.marquee:hover .track, .marquee:focus-within .track { animation-play-state: paused; }\n.hint { margin: 0; text-align: center; font: 400 13px/1.4 system-ui, sans-serif; color: #8b93a7; }\n@media (prefers-reduced-motion: reduce) { .track { animation: none; } }",
        "js": "/* No JavaScript required — duplicated track + transform keyframes. */"
      },
      "source": {
        "name": "CSS-Tricks — CSS-Only Marquee Effect",
        "url": "https://css-tricks.com/css-only-marquee-effect/",
        "domain": "css-tricks.com"
      },
      "performance": "transform: translateX — compositor-only; трек дублирован (2× DOM), держать его коротким; mask-image добавляет отдельный слой.",
      "a11y": "Дублирующий трек скрыт aria-hidden, текст читается один раз; reduced-motion останавливает ленту; пауза также по hover и focus-within.",
      "browser_support": "Chrome 90+, Safari 15.4+, Firefox 103+ (mask-image с градиентом).",
      "ai_use": "Marquee a duplicated headline track (second copy aria-hidden) with translateX 0→-50% 16s linear infinite, fade the edges with mask-image, pause on hover/focus-within; static under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-count-up-odometer",
      "name": "Odometer Count-Up on Scroll",
      "name_ru": "Одометр-счётчик по скроллу",
      "category": "text",
      "tags": [
        "count-up",
        "odometer",
        "numbers",
        "scroll",
        "intersection-observer"
      ],
      "summary": "Статистика на скролле: каждая цифра — барабан 0–9, который докручивается до нужного значения со стаггером по разрядам, как одометр.",
      "effect": "IntersectionObserver (threshold .5) триггерит: цифровые колонки translateY(-N em) за 1.2 с cubic-bezier(.2,.7,.2,1) с задержкой 90 мс на разряд; срабатывает один раз.",
      "when_to_use": "Секция результатов и метрик: «128+ интеграций», «24k разработчиков», «97% uptime» — после блока фич.",
      "tech": [
        "vanilla-js",
        "css",
        "intersection-observer"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo\">\n  <section class=\"stats\" aria-label=\"Results\">\n    <article class=\"stat\" aria-label=\"128 plus integrations shipped\">\n      <p class=\"value\" aria-hidden=\"true\"><span class=\"num\" data-value=\"128\"></span><span class=\"suffix\">+</span></p>\n      <p class=\"label\">integrations shipped</p>\n    </article>\n    <article class=\"stat\" aria-label=\"24 thousand developers onboard\">\n      <p class=\"value\" aria-hidden=\"true\"><span class=\"num\" data-value=\"24\"></span><span class=\"suffix\">k</span></p>\n      <p class=\"label\">developers onboard</p>\n    </article>\n    <article class=\"stat\" aria-label=\"97 percent uptime last quarter\">\n      <p class=\"value\" aria-hidden=\"true\"><span class=\"num\" data-value=\"97\"></span><span class=\"suffix\">%</span></p>\n      <p class=\"label\">uptime last quarter</p>\n    </article>\n  </section>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 620px; display: grid; align-content: end;\n  padding: 40px 24px; box-sizing: border-box;\n  background: #0b0d12; color: #f8fafc;\n}\n.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }\n.value {\n  display: flex; align-items: baseline; margin: 0;\n  font: 800 clamp(30px, 9vw, 44px)/1 ui-monospace, SFMono-Regular, Menlo, monospace;\n  color: #7dd3fc;\n}\n.suffix { margin-left: 2px; font-size: 0.55em; }\n.label { margin: 8px 0 0; font: 400 13px/1.35 system-ui, sans-serif; color: #94a3b8; }\n.reel { display: inline-block; height: 1em; overflow: hidden; }\n.strip {\n  display: flex; flex-direction: column; transform: translateY(0);\n  transition: transform 1.2s cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n.strip i { height: 1em; line-height: 1; font-style: normal; }\n@media (prefers-reduced-motion: reduce) { .strip { transition: none; } }",
        "js": "const stats = document.querySelectorAll('.stat');\nstats.forEach((stat) => {\n  const num = stat.querySelector('.num');\n  num.innerHTML = num.dataset.value.split('').map((d, i) => {\n    const reel = [...Array(10).keys()].map((n) => `<i>${n}</i>`).join('');\n    return `<span class=\"reel\"><span class=\"strip\" data-d=\"${d}\" style=\"transition-delay:${i * 90}ms\">${reel}</span></span>`;\n  }).join('');\n});\nconst io = new IntersectionObserver((entries) => {\n  entries.forEach((entry) => {\n    if (!entry.isIntersecting) return;\n    entry.target.querySelectorAll('.strip').forEach((s) => {\n      s.style.transform = `translateY(-${s.dataset.d}em)`;\n    });\n    io.unobserve(entry.target);\n  });\n}, { threshold: 0.5 });\nstats.forEach((s) => io.observe(s));"
      },
      "source": {
        "name": "CSS-Tricks — Animating Number Counters",
        "url": "https://css-tricks.com/animating-number-counters/",
        "domain": "css-tricks.com"
      },
      "performance": "IO + единичные записи transform на reels (компоситор), не чаще одного раза на секцию; 1.2 с transition на десятке колонок — дёшево.",
      "a11y": "Колёсики цифр скрыты aria-hidden, финальное значение вынесено в aria-label статьи вместе с суффиксом; reduced-motion убирает transition — числа меняются мгновенно.",
      "browser_support": "Chrome 51+, Safari 12.1+, Firefox 55+ (IntersectionObserver).",
      "ai_use": "When the metrics section enters the viewport, roll per-digit odometer reels to each target number (1.2s ease-out, 90ms stagger per digit); instant jump under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-rotate-words",
      "name": "Rotating Words Swap",
      "name_ru": "Ротация слов в заголовке",
      "category": "text",
      "tags": [
        "rotator",
        "words",
        "css-only",
        "loop",
        "hero"
      ],
      "summary": "Слово в заголовке меняется по кругу: build → ship → scale; слова поочерёдно выезжают снизу и уходят вверх в одной и той же позиции.",
      "effect": "Цикл 12 с, задержки слов 0/4/8 с: opacity 0→1 и translateY 0.7em→0 на отрезке 3–30% слота, уход вверх к 33%. Слова лежат в одной grid-ячейке — ширина не прыгает.",
      "when_to_use": "Hero-заголовок «We help teams build X», ротатор УТП в заголовке секции.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <h2 class=\"rotator\">\n    We help teams\n    <span class=\"words\">\n      <span style=\"--d:0s\">build</span>\n      <span style=\"--d:4s\">ship</span>\n      <span style=\"--d:8s\">scale</span>\n    </span>\n  </h2>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center;\n  padding: 24px; background: #0b0d12; color: #eef2ff;\n  font: 700 clamp(26px, 7.5vw, 38px)/1.35 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n}\n.rotator { margin: 0; }\n.words { display: inline-grid; vertical-align: bottom; }\n.words span {\n  grid-area: 1 / 1; justify-self: start;\n  opacity: 0; transform: translateY(0.7em); color: #38bdf8;\n  animation: rotate-words 12s cubic-bezier(0.2, 0.7, 0.2, 1) infinite;\n  animation-delay: var(--d);\n}\n@keyframes rotate-words {\n  0% { opacity: 0; transform: translateY(0.7em); }\n  3%, 30% { opacity: 1; transform: translateY(0); }\n  33%, 100% { opacity: 0; transform: translateY(-0.7em); }\n}\n@media (prefers-reduced-motion: reduce) {\n  .words span { animation: none; opacity: 0; transform: none; }\n  .words span:first-child { opacity: 1; }\n}",
        "js": "/* No JavaScript required — staggered CSS keyframes over one 12s cycle. */"
      },
      "source": {
        "name": "Codrops — Rotating Words with CSS Animations",
        "url": "https://tympanus.net/codrops/2012/04/17/rotating-words-with-css-animations/",
        "domain": "tympanus.net"
      },
      "performance": "opacity/transform по CSS-анимации — compositor; три слова, статичный вес, без пересчёта layout.",
      "a11y": "prefers-reduced-motion: анимация выключена, показывается первое слово; слова лежат в DOM в обычном порядке и читаются.",
      "browser_support": "Chrome 43+, Safari 9+, Firefox 16+ (CSS animations, inline-grid).",
      "ai_use": "Rotate 3–4 words in place inside a headline: each word fades in from +0.7em and exits upward at 4s offsets over a 12s cycle; show only the first word under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-wave-hover",
      "name": "Wave Hover (Per-Letter Bounce)",
      "name_ru": "Волна по буквам при наведении",
      "category": "text",
      "tags": [
        "wave",
        "hover",
        "letters",
        "stagger",
        "playful"
      ],
      "summary": "При наведении буквы по очереди подпрыгивают вверх и подсвечиваются акцентом — по слову пробегает волна.",
      "effect": "Каждая буква: translateY(-14px) и смена цвета за 350 мс cubic-bezier(.34,1.56,.64,1) с лёгким overshoot, задержка calc(var(--i) * 60ms); при уходе курсора возврат без задержки.",
      "when_to_use": "CTA-надпись, ссылка в меню, интерактивный заголовок в hero или футере.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <p class=\"wave\" aria-label=\"Motion\">\n    <span aria-hidden=\"true\" style=\"--i:0\">M</span><span aria-hidden=\"true\" style=\"--i:1\">O</span><span aria-hidden=\"true\" style=\"--i:2\">T</span><span aria-hidden=\"true\" style=\"--i:3\">I</span><span aria-hidden=\"true\" style=\"--i:4\">O</span><span aria-hidden=\"true\" style=\"--i:5\">N</span>\n  </p>\n  <p class=\"hint\">hover the word</p>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; justify-items: center; gap: 14px;\n  padding: 24px; background: #0b0d12;\n}\n.wave {\n  margin: 0;\n  font: 800 clamp(32px, 11vw, 52px)/1 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  letter-spacing: 0.12em;\n}\n.wave span {\n  display: inline-block; color: #f8fafc;\n  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.35s ease;\n}\n.wave:hover span {\n  transform: translateY(-14px); color: #38bdf8;\n  transition-delay: calc(var(--i) * 60ms);\n}\n.hint { margin: 0; font: 400 13px/1.4 system-ui, sans-serif; color: #8b93a7; }\n@media (prefers-reduced-motion: reduce) {\n  .wave span, .wave:hover span { transition: none; transform: none; }\n}",
        "js": "/* No JavaScript required — per-letter spans with transition-delay. */"
      },
      "source": {
        "name": "Karim Ould — Hover Letter Wave Effect in CSS",
        "url": "https://blog.karimould.dev/hover-letter-wave-effect-in-css",
        "domain": "blog.karimould.dev"
      },
      "performance": "transform/color через transition — compositor; срабатывает только на hover, без влияния на страницу в покое.",
      "a11y": "prefers-reduced-motion отключает transition и подъём; слово читается через aria-label=\"Motion\", сами буквы aria-hidden.",
      "browser_support": "Chrome 49+, Safari 10+, Firefox 32+ (transition-delay, custom properties).",
      "ai_use": "On hover of a word, lift each letter -14px and tint it accent color, 350ms spring cubic-bezier(.34,1.56,.64,1) with 60ms per-letter delay; no transform under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": ".wave",
        "wait_ms": 1600,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-fill-gradient-clip",
      "name": "Gradient-Filled Text (background-clip)",
      "name_ru": "Текст, залитый градиентом через background-clip",
      "category": "text",
      "tags": [
        "background-clip",
        "gradient",
        "text-fill",
        "drift",
        "display-type"
      ],
      "summary": "Крупное слово залито многослойным градиентом через background-clip: text; градиент медленно дрейфует внутри букв.",
      "effect": "Три radial-gradient (розовый, голубой, жёлтый) на фиолетовой базе, background-size 170%, дрейф background-position 0%→100% за 9 с ease-in-out alternate.",
      "when_to_use": "Одно слово в hero, название продукта, крупная цифра; только display-типографика, не для абзацев.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo\">\n  <h1 class=\"fill\">Aurora</h1>\n  <p class=\"hint\">gradient clipped to glyphs, slow drift</p>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; align-content: center; justify-items: center; gap: 12px;\n  padding: 24px; background: #0b0d12; text-align: center;\n}\n.fill {\n  margin: 0;\n  font: 900 clamp(56px, 18vw, 96px)/1 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  letter-spacing: -0.03em;\n  color: #c4b5fd;\n  background:\n    radial-gradient(120% 100% at 18% 18%, #f472b6 0%, rgba(244, 114, 182, 0) 55%),\n    radial-gradient(120% 100% at 82% 28%, #38bdf8 0%, rgba(56, 189, 248, 0) 60%),\n    radial-gradient(140% 120% at 50% 92%, #facc15 0%, rgba(250, 204, 21, 0) 55%),\n    #7c3aed;\n  background-size: 170% 170%;\n  animation: drift 9s ease-in-out infinite alternate;\n}\n@supports (background-clip: text) or (-webkit-background-clip: text) {\n  .fill {\n    -webkit-background-clip: text; background-clip: text;\n    color: transparent; -webkit-text-fill-color: transparent;\n  }\n}\n@keyframes drift {\n  from { background-position: 0% 0%; }\n  to { background-position: 100% 100%; }\n}\n.hint { margin: 0; font: 400 13px/1.4 system-ui, sans-serif; color: #8b93a7; }\n@media (prefers-reduced-motion: reduce) {\n  .fill { animation: none; background-position: 50% 40%; }\n}",
        "js": "/* No JavaScript required — gradient plus background-clip: text. */"
      },
      "source": {
        "name": "MDN — background-clip (text)",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/background-clip",
        "domain": "developer.mozilla.org"
      },
      "performance": "background-position на градиенте с background-clip: text — paint каждый кадр; держать на одном display-элементе.",
      "a11y": "prefers-reduced-motion останавливает дрейф; @supports даёт fallback-цвет #c4b5fd, если background-clip: text не поддерживается — текст не пропадает.",
      "browser_support": "background-clip: text без префикса — Chrome 120+, Edge 120+, Safari 15.5+, Firefox 49+.",
      "ai_use": "Fill a big word with a multi-radial gradient via background-clip:text (transparent fill) and slowly drift background-position 9s alternate; keep a solid fallback color and freeze the drift under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "text-glitch-rgb-split",
      "name": "Glitch RGB Split",
      "name_ru": "Глитч с RGB-раздвоением",
      "category": "text",
      "tags": [
        "glitch",
        "rgb-split",
        "clip-path",
        "monospace",
        "cyber"
      ],
      "summary": "RGB-split глитч: поверх заголовка вспыхивают красная и голубая сдвинутые копии с рваными срезами; всплеск короткий, затем текст чистый, и так по кругу.",
      "effect": "::before/::after с content: attr(data-text), text-shadow ±2px (#f43f5e/#22d3ee) и clip-path inset-срезами; анимация steps(1) 3.4 с: всплеск ~0.6 с в начале цикла, остальное время копии скрыты.",
      "when_to_use": "Техно/гейминг лендинг, заголовок в тёмной hero-секции, акцент запуска продукта; один элемент на страницу.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo\">\n  <h1 class=\"glitch\" data-text=\"SYSTEM ONLINE\">SYSTEM ONLINE</h1>\n</div>",
        "css": "body { margin: 0; }\n.demo {\n  min-height: 320px; display: grid; place-content: center;\n  padding: 24px; background: #0b0d12;\n}\n.glitch {\n  position: relative; margin: 0;\n  font: 800 clamp(30px, 9vw, 48px)/1.1 ui-monospace, SFMono-Regular, Menlo, monospace;\n  letter-spacing: 0.06em; color: #f8fafc;\n}\n.glitch::before, .glitch::after {\n  content: attr(data-text); position: absolute; inset: 0;\n}\n.glitch::before {\n  left: 2px; text-shadow: -2px 0 #f43f5e; clip-path: inset(0 0 100% 0);\n  animation: slice-a 3.4s steps(1) infinite;\n}\n.glitch::after {\n  left: -2px; text-shadow: 2px 0 #22d3ee; clip-path: inset(100% 0 0 0);\n  animation: slice-b 3.4s steps(1) infinite;\n}\n@keyframes slice-a {\n  0%, 100% { clip-path: inset(0 0 100% 0); transform: none; }\n  4% { clip-path: inset(10% 0 62% 0); transform: translateX(-3px); }\n  9% { clip-path: inset(74% 0 8% 0); transform: translateX(2px); }\n  13% { clip-path: inset(34% 0 44% 0); transform: translateX(-2px); }\n  17% { clip-path: inset(0 0 100% 0); transform: none; }\n}\n@keyframes slice-b {\n  0%, 100% { clip-path: inset(100% 0 0 0); transform: none; }\n  5% { clip-path: inset(56% 0 18% 0); transform: translateX(3px); }\n  11% { clip-path: inset(20% 0 66% 0); transform: translateX(-2px); }\n  15% { clip-path: inset(80% 0 4% 0); transform: translateX(2px); }\n  19% { clip-path: inset(100% 0 0 0); transform: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .glitch::before, .glitch::after { animation: none; }\n}",
        "js": "/* No JavaScript required — pseudo-element slices with clip-path keyframes. */"
      },
      "source": {
        "name": "CSS-Tricks — Glitch Effect on Text / Images / SVG",
        "url": "https://css-tricks.com/glitch-effect-text-images-svg/",
        "domain": "css-tricks.com"
      },
      "performance": "clip-path у псевдоэлементов с steps() — paint, но всплески короткие и элемент один; не размножать по странице.",
      "a11y": "Псевдоэлементы не читаются скринридером, текст — обычный h1; prefers-reduced-motion отключает срезы, копии остаются скрытыми по базовому clip-path.",
      "browser_support": "clip-path: inset() — Chrome 55+, Safari 9.1+, Firefox 54+.",
      "ai_use": "Apply an RGB-split glitch to a mono heading: ::before/::after copies with ±2px red/cyan text-shadow and stepped clip-path slices bursting ~0.6s every 3.4s; disable slices under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1000,
        "bg": "#0b0d12"
      },
      "_file": "agent-02-text.json"
    },
    {
      "id": "scroll-multilayer-parallax",
      "name": "Multi-layer Parallax",
      "name_ru": "Многослойный параллакс",
      "category": "scroll",
      "tags": [
        "parallax",
        "translate3d",
        "vanilla-js",
        "scroll-linked",
        "layers"
      ],
      "summary": "Hero-секция с четырьмя слоями — небо, солнце, два горных хребта и текст — двигаются с разной скоростью относительно скролла, создавая ощущение глубины без картинок, только CSS-градиенты и inline SVG.",
      "effect": "Коэффициенты 0.08 / 0.22 / 0.4 / 0.62 от прокрутки внутри секции: позиция читается один раз за кадр в rAF и пишется в translate3d(0, y*speed, 0). Связь линейная 1:1 со скроллом, слои ограничены высотой секции, easing не нужен.",
      "when_to_use": "Hero лендинга, обложки кейсов, промо-блоки с 2–4 декоративными слоями фона. Не более 3–4 слоёв на страницу и никогда для мелкого текста.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class='plx'>\n  <div class='plx-layer plx-sky' data-speed='0.08'></div>\n  <div class='plx-layer plx-sun' data-speed='0.22'></div>\n  <div class='plx-layer plx-hills-back' data-speed='0.4'>\n    <svg viewBox='0 0 480 160' preserveAspectRatio='none'><path d='M0 160 90 60l80 70 70-90 90 100 80-60 70 80z' fill='#1c4b63'/></svg>\n  </div>\n  <div class='plx-layer plx-hills-front' data-speed='0.62'>\n    <svg viewBox='0 0 480 160' preserveAspectRatio='none'><path d='M0 160 70 90l90 50 90-70 100 90 70-40 60 40z' fill='#0e2433'/></svg>\n  </div>\n  <div class='plx-copy'>\n    <p class='plx-kicker'>Parallax</p>\n    <h1>Three depths,<br>one scroll</h1>\n  </div>\n</section>\n<section class='plx-next'><p>Keep scrolling — layers move at 0.08, 0.22 and 0.4 of scroll speed.</p></section>",
        "css": ".plx { position: relative; height: 320px; overflow: hidden; background: linear-gradient(#071522, #0b2b40 70%, #123c55); }\n.plx-layer { position: absolute; inset: -20% -10%; will-change: transform; }\n.plx-sun { inset: auto; left: 58%; top: 58px; width: 76px; height: 76px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #ffd98a, #f59e0b 60%, rgba(245,158,11,0)); }\n.plx-hills-back, .plx-hills-front { inset: auto 0 -10% 0; height: 62%; }\n.plx-hills-back svg, .plx-hills-front svg { width: 100%; height: 100%; display: block; }\n.plx-copy { position: absolute; left: 24px; bottom: 26px; }\n.plx-kicker { margin: 0 0 6px; color: #7dd3fc; font: 600 11px/1 system-ui; letter-spacing: .18em; text-transform: uppercase; }\n.plx-copy h1 { margin: 0; color: #f8fafc; font: 700 30px/1.1 system-ui; }\n.plx-next { min-height: 420px; padding: 24px; background: #0b0d12; color: #94a3b8; font: 400 14px/1.6 system-ui; }\n@media (prefers-reduced-motion: reduce) { .plx-layer { will-change: auto; } }",
        "js": "const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst sec = document.querySelector('.plx');\nconst layers = [...document.querySelectorAll('.plx-layer')];\nif (!reduce) {\n  let ticking = false;\n  const update = () => {\n    ticking = false;\n    const y = Math.min(Math.max(window.scrollY - sec.offsetTop, 0), sec.offsetHeight);\n    layers.forEach((el) => {\n      const s = parseFloat(el.dataset.speed);\n      el.style.transform = `translate3d(0, ${(y * s).toFixed(1)}px, 0)`;\n    });\n  };\n  addEventListener('scroll', () => {\n    if (!ticking) { ticking = true; requestAnimationFrame(update); }\n  }, { passive: true });\n  update();\n}"
      },
      "source": {
        "name": "web.dev — Parallaxin' (оптимизация параллакса: rAF + 3D-трансформы)",
        "url": "https://web.dev/articles/speed-parallax",
        "domain": "web.dev"
      },
      "performance": "Compositor-only: только transform, пассивный scroll-листенер и один rAF-кадр; layout не читается в цикле. Держать 3–4 слоя и убирать will-change после анимации.",
      "a11y": "prefers-reduced-motion: проверка в JS — слои не трансформируются вовсе, плюс media query для отключения will-change. Скролл нативный, без scroll-jacking.",
      "browser_support": "Все современные браузеры: translate3d, rAF и dataset — Chrome, Safari, Firefox, Edge (baseline).",
      "ai_use": "On hero scroll, translate 2–4 absolutely-positioned layers by scrollY × 0.08–0.62 inside one rAF-throttled listener using translate3d; skip transforms under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-mouse-scroll-parallax",
      "name": "Mouse + Scroll Parallax Hero",
      "name_ru": "Параллакс hero: мышь и скролл",
      "category": "scroll",
      "tags": [
        "parallax",
        "mousemove",
        "hero",
        "gsap",
        "quickto"
      ],
      "summary": "Hero, где четыре слоя реагируют одновременно на курсор и на прокрутку: мышь сдвигает слои по X и Y через gsap.quickTo, а скролл добавляет каждому слою собственное смещение вниз.",
      "effect": "Мышь: x = ±40px × глубина (0.6–3.0), yPercent = ±16% × глубина, сглаживание 0.5s power3 через quickTo. Скролл: scrub-твин y = 0→120px × скорость (0.08–0.5) за проход секции, start 'top top', end 'bottom top'.",
      "when_to_use": "Hero главной или продуктовой страницы, где нужен живой отклик на курсор и глубина при прокрутке. На тач-устройствах остаётся только скролл-часть.",
      "tech": [
        "gsap",
        "vanilla-js"
      ],
      "dependencies": [
        "gsap 3.13 + ScrollTrigger (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js'></script>\n<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js'></script>\n<section class='hero'>\n  <div class='hero-glow' data-s='0.08' data-m='0.6'></div>\n  <div class='hero-ring' data-s='0.2' data-m='1.4'></div>\n  <div class='hero-dots' data-s='0.34' data-m='2.2'></div>\n  <div class='hero-card' data-s='0.5' data-m='3'>\n    <h1>Aurora UI</h1>\n    <p>Components that follow your cursor.</p>\n    <button type='button'>Get started</button>\n  </div>\n</section>\n<section class='hero-next'><p>Move the mouse and scroll — every layer reacts at its own depth.</p></section>",
        "css": ".hero { position: relative; height: 320px; overflow: hidden; background: #0b0d12; }\n.hero > div { position: absolute; }\n.hero-glow { left: -80px; top: -120px; width: 360px; height: 360px; border-radius: 50%; background: radial-gradient(circle, rgba(56,189,248,.4), transparent 65%); }\n.hero-ring { right: -40px; top: 30px; width: 200px; height: 200px; border: 1.5px solid rgba(167,139,250,.5); border-radius: 50%; }\n.hero-dots { inset: 0; background-image: radial-gradient(rgba(255,255,255,.22) 1px, transparent 1.5px); background-size: 26px 26px; }\n.hero-card { left: 26px; top: 70px; padding: 18px 20px; border-radius: 14px; background: rgba(23,27,38,.92); border: 1px solid #22d3ee33; box-shadow: 0 18px 40px #0009; }\n.hero-card h1 { margin: 0 0 6px; color: #f8fafc; font: 700 26px/1 system-ui; }\n.hero-card p { margin: 0 0 12px; color: #94a3b8; font: 400 13px/1.5 system-ui; }\n.hero-card button { padding: 8px 14px; border: 0; border-radius: 8px; background: #22d3ee; color: #04121a; font: 700 12px system-ui; }\n.hero-next { min-height: 420px; padding: 24px; background: #0b0d12; color: #64748b; font: 400 14px/1.6 system-ui; }\n@media (prefers-reduced-motion: reduce) { .hero > div { transition: none; } }",
        "js": "gsap.registerPlugin(ScrollTrigger);\nconst hero = document.querySelector('.hero');\nif (!matchMedia('(prefers-reduced-motion: reduce)').matches) {\n  const layers = gsap.utils.toArray('.hero > div').map((el) => ({\n    el,\n    m: parseFloat(el.dataset.m),\n    xTo: gsap.quickTo(el, 'x', { duration: 0.5, ease: 'power3' }),\n    yTo: gsap.quickTo(el, 'yPercent', { duration: 0.5, ease: 'power3' })\n  }));\n  hero.addEventListener('mousemove', (e) => {\n    const r = hero.getBoundingClientRect();\n    const mx = (e.clientX - r.left) / r.width - 0.5;\n    const my = (e.clientY - r.top) / r.height - 0.5;\n    layers.forEach((l) => { l.xTo(mx * 40 * l.m); l.yTo(my * 16 * l.m); });\n  });\n  layers.forEach((l) => {\n    gsap.to(l.el, {\n      y: 120 * parseFloat(l.el.dataset.s),\n      ease: 'none',\n      scrollTrigger: { trigger: hero, start: 'top top', end: 'bottom top', scrub: 1 }\n    });\n  });\n}"
      },
      "source": {
        "name": "GSAP Docs — gsap.quickTo() (оптимизация mousemove-анимаций)",
        "url": "https://gsap.com/docs/v3/GSAP/gsap.quickTo()/",
        "domain": "gsap.com"
      },
      "performance": "quickTo переиспользует один твин — ноль аллокаций на каждый mousemove; скролл-часть — scrub-твин ScrollTrigger, всё пишется в transform (compositor). Глубину слоёв держать ≤ 3.",
      "a11y": "prefers-reduced-motion: параллакс не инициализируется вовсе (ни мышь, ни скролл), элементы стоят на своих местах, скролл нативный.",
      "browser_support": "Везде, где работает GSAP 3 (Chrome 90+, Safari 15+, Firefox 90+, Edge 90+).",
      "ai_use": "In a hero, combine pointer parallax (gsap.quickTo on x/yPercent, 0.5s power3) with a ScrollTrigger scrub tween on y per layer; disable everything under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": ".hero",
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-sticky-scrollytelling",
      "name": "Sticky Scrollytelling",
      "name_ru": "Скроллителлинг с липкой графикой",
      "category": "scroll",
      "tags": [
        "sticky",
        "scrollytelling",
        "intersection-observer",
        "steps",
        "highlight"
      ],
      "summary": "Секция-история: карточка-графика липнет к верху экрана и меняет номер и цвет, пока три текстовых шага прокручиваются мимо; активный шаг подсвечивается цветом без появления и исчезновения.",
      "effect": "IntersectionObserver с rootMargin -46% ловит шаг в центральной полосе вьюпорта. Активный шаг красит заголовок в #22d3ee, карточка меняет градиент по data-step (0.4s ease), точки-индикаторы переключаются с scale(1.4) за 0.3s.",
      "when_to_use": "Кейс-стади, блок «как мы работаем», процесс из 3–5 шагов, где одна визуализация удерживается на экране, пока меняется рассказ.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class='story'>\n  <div class='story-graphic'>\n    <div class='story-visual' id='sv'>\n      <span class='story-index'>01</span>\n      <span class='story-label'>Research</span>\n    </div>\n    <div class='story-dots'><i class='on'></i><i></i><i></i></div>\n  </div>\n  <div class='story-steps'>\n    <article class='step is-active' data-label='Research' data-index='01'><h3>Research</h3><p>We map the funnel and find drop-offs.</p></article>\n    <article class='step' data-label='Design' data-index='02'><h3>Design</h3><p>Wireframes turn into a system of blocks.</p></article>\n    <article class='step' data-label='Launch' data-index='03'><h3>Launch</h3><p>Ship, measure, iterate every week.</p></article>\n  </div>\n</section>\n<section class='story-end'><p>That is the whole sticky-graphic pattern.</p></section>",
        "css": ".story { position: relative; background: #0b0d12; }\n.story-graphic { position: sticky; top: 0; z-index: 2; display: grid; place-items: center; height: 214px; background: #0b0d12; border-bottom: 1px solid #1e293b; }\n.story-visual { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 170px; padding: 22px 0; border-radius: 16px; background: linear-gradient(160deg, #123047, #0f172a); border: 1px solid #22d3ee55; transition: background .4s ease, border-color .4s ease; }\n.story-visual[data-step='2'] { background: linear-gradient(160deg, #3b2650, #0f172a); border-color: #a78bfa77; }\n.story-visual[data-step='3'] { background: linear-gradient(160deg, #14432e, #0f172a); border-color: #34d39977; }\n.story-index { font: 800 34px/1 system-ui; color: #f8fafc; }\n.story-label { font: 600 12px system-ui; letter-spacing: .14em; text-transform: uppercase; color: #7dd3fc; }\n.story-dots { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); display: grid; gap: 6px; }\n.story-dots i { width: 6px; height: 6px; border-radius: 50%; background: #334155; transition: background .3s, transform .3s; }\n.story-dots i.on { background: #22d3ee; transform: scale(1.4); }\n.step { display: grid; place-content: center; gap: 6px; min-height: 300px; padding: 0 30px; text-align: center; }\n.step h3 { margin: 0; color: #e2e8f0; font: 700 20px/1 system-ui; transition: color .3s; }\n.step p { margin: 0; color: #64748b; font: 400 14px/1.5 system-ui; }\n.step.is-active h3 { color: #22d3ee; }\n.story-end { min-height: 260px; display: grid; place-content: center; color: #475569; font: 400 14px system-ui; background: #0b0d12; }\n@media (prefers-reduced-motion: reduce) { .story-visual, .story-dots i, .step h3 { transition: none; } }",
        "js": "const steps = [...document.querySelectorAll('.step')];\nconst visual = document.getElementById('sv');\nconst index = visual.querySelector('.story-index');\nconst label = visual.querySelector('.story-label');\nconst dots = [...document.querySelectorAll('.story-dots i')];\nconst io = new IntersectionObserver((entries) => {\n  entries.forEach((en) => {\n    if (!en.isIntersecting) return;\n    const s = en.target;\n    const i = steps.indexOf(s);\n    steps.forEach((x) => x.classList.toggle('is-active', x === s));\n    visual.dataset.step = s.dataset.index;\n    index.textContent = s.dataset.index;\n    label.textContent = s.dataset.label;\n    dots.forEach((d, j) => d.classList.toggle('on', j === i));\n  });\n}, { rootMargin: '-46% 0px -46% 0px' });\nsteps.forEach((s) => io.observe(s));"
      },
      "source": {
        "name": "The Pudding — Easier scrollytelling with position sticky",
        "url": "https://pudding.cool/process/scrollytelling-sticky/",
        "domain": "pudding.cool"
      },
      "performance": "IntersectionObserver вместо scroll-листенеров — ноль работы на кадр скролла; transitions только по background/color/border (paint, не layout); sticky бесплатен. На мобильных при 100vh-секциях следить за высотой шагов.",
      "a11y": "prefers-reduced-motion убирает все transitions; подсветка шага и текст остаются, контент читается линейно; скролл не блокируется.",
      "browser_support": "sticky и IntersectionObserver: Chrome 56+, Safari 13+, Firefox 55+ (baseline).",
      "ai_use": "Build a sticky-graphic scrollytelling block: sticky 210px visual on top, 300px text steps below, IntersectionObserver with rootMargin -46% swaps step index/label and highlight color; no fades, no scroll-jacking.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3500,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-gsap-pin-scrub",
      "name": "ScrollTrigger Pin + Scrub Timeline",
      "name_ru": "Пин-секция с таймлайном по скрабу",
      "category": "scroll",
      "tags": [
        "gsap",
        "scrolltrigger",
        "pin",
        "scrub",
        "timeline"
      ],
      "summary": "Секция фиксируется на экране, и одна GSAP-таймлайн управляется скроллом: карточка растёт и вращается на 360°, прогресс-бар заполняется, три шага по очереди подсвечиваются.",
      "effect": "ScrollTrigger pin: true, start 'top top', end '+=900', scrub: 1 (плейхед догоняет скроллбар за секунду). В таймлайне: scaleX бара 0→1 на всю длину, rotation 360° и y −26px у карточки (power1.inOut), подсветка шагов на позициях 0.4 / 1.6 / 2.8 с длительностью 0.6.",
      "when_to_use": "Блоки «как это работает», разбор продукта по шагам, сборка/настройка, где важно удержать внимание на одной сцене и вести пользователя через этапы.",
      "tech": [
        "gsap"
      ],
      "dependencies": [
        "gsap 3.13 + ScrollTrigger (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js'></script>\n<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js'></script>\n<section class='pin-scene'>\n  <div class='pin-stage'>\n    <h2>Three moves to launch</h2>\n    <div class='pin-card'><span class='pin-badge'>3.13</span><strong>Timeline</strong></div>\n    <ul class='pin-steps'><li>Configure</li><li>Connect</li><li>Launch</li></ul>\n    <div class='pin-bar'><i></i></div>\n  </div>\n</section>\n<section class='pin-after'><p>One timeline, fully scrubbed by the scrollbar.</p></section>",
        "css": ".pin-scene { height: 100vh; background: #0b0d12; }\n.pin-stage { display: grid; place-content: center; justify-items: center; gap: 18px; height: 100%; }\n.pin-stage h2 { margin: 0; color: #f8fafc; font: 700 22px/1 system-ui; }\n.pin-card { display: grid; place-items: center; gap: 4px; width: 130px; padding: 16px 0; border-radius: 14px; background: #111827; border: 1px solid #334155; color: #e2e8f0; font: 600 13px system-ui; }\n.pin-badge { color: #22d3ee; font: 700 12px/1 ui-monospace, monospace; }\n.pin-steps { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }\n.pin-steps li { padding: 6px 14px; border-radius: 999px; background: #1e293b; color: #94a3b8; font: 600 12px system-ui; }\n.pin-bar { width: 160px; height: 4px; border-radius: 999px; background: #1e293b; overflow: hidden; }\n.pin-bar i { display: block; height: 100%; background: #22d3ee; transform: scaleX(0); transform-origin: left; }\n.pin-after { min-height: 360px; display: grid; place-content: center; color: #64748b; font: 400 14px system-ui; background: #0b0d12; }\n@media (prefers-reduced-motion: reduce) { .pin-scene { height: auto; padding: 40px 0; } }",
        "js": "gsap.registerPlugin(ScrollTrigger);\nif (!matchMedia('(prefers-reduced-motion: reduce)').matches) {\n  const tl = gsap.timeline({\n    scrollTrigger: { trigger: '.pin-scene', start: 'top top', end: '+=900', pin: true, scrub: 1 }\n  });\n  tl.to('.pin-bar i', { scaleX: 1, ease: 'none', duration: 3 }, 0)\n    .to('.pin-card', { y: -26, scale: 1.12, rotation: 360, ease: 'power1.inOut', duration: 3 }, 0);\n  gsap.utils.toArray('.pin-steps li').forEach((li, i) => {\n    tl.to(li, { backgroundColor: '#22d3ee', color: '#04121a', duration: 0.6 }, i * 1.2 + 0.4);\n  });\n}"
      },
      "source": {
        "name": "GSAP Docs — ScrollTrigger (pin, scrub, timeline)",
        "url": "https://gsap.com/docs/v3/Plugins/ScrollTrigger/",
        "domain": "gsap.com"
      },
      "performance": "Анимируются только transform и цвета внутри пиннированной сцены; GSAP сам добавляет pin-spacer. Не менять layout pinned-элемента и вызывать ScrollTrigger.refresh() после загрузки шрифтов и картинок.",
      "a11y": "prefers-reduced-motion: пин и таймлайн не создаются, секция становится обычной высотой auto — весь контент виден сразу, принудительной прокрутки нет.",
      "browser_support": "Везде, где работает GSAP 3 + ScrollTrigger (Chrome 90+, Safari 15+, Firefox 90+).",
      "ai_use": "Pin a 100vh section and scrub one GSAP timeline over +=900px: progress bar scaleX 0→1, card scale/rotate, step pills highlight sequentially; no pin under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 4000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-horizontal-pin",
      "name": "Horizontal Scroll Section (Pin)",
      "name_ru": "Горизонтальная секция на пине",
      "category": "scroll",
      "tags": [
        "gsap",
        "scrolltrigger",
        "horizontal",
        "pin",
        "xpercent"
      ],
      "summary": "Вертикальный скролл превращается в горизонтальное движение: секция фиксируется, а лента из трёх панелей проезжает справа налево через xPercent.",
      "effect": "gsap.to(track, { xPercent: -100 * (n−1), ease: 'none' }) со ScrollTrigger: pin: true, scrub: 1, end = ширина трека минус ширина окна, invalidateOnRefresh при ресайзе. Связь строго линейная: пиксель вертикального скролла = пиксель горизонтального хода.",
      "when_to_use": "Портфолио, таймлайны, «этапы проекта», галереи из 3–5 панелей; хорошо идёт сразу после hero как смена ритма страницы.",
      "tech": [
        "gsap"
      ],
      "dependencies": [
        "gsap 3.13 + ScrollTrigger (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js'></script>\n<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js'></script>\n<section class='h-scene'>\n  <div class='h-track'>\n    <article class='h-panel p1'><span>01</span><h3>Discover</h3><p>Scroll vertically — the track moves sideways.</p></article>\n    <article class='h-panel p2'><span>02</span><h3>Design</h3><p>Each panel is one step of the story.</p></article>\n    <article class='h-panel p3'><span>03</span><h3>Deliver</h3><p>Pin releases when the last panel lands.</p></article>\n  </div>\n</section>\n<section class='h-after'><p>Vertical scroll, horizontal motion.</p></section>",
        "css": ".h-scene { height: 100vh; overflow: hidden; background: #0b0d12; }\n.h-track { display: flex; height: 100%; }\n.h-panel { flex: 0 0 100vw; display: grid; place-content: center; gap: 8px; padding: 0 40px; text-align: center; }\n.h-panel span { font: 800 40px/1 ui-monospace, monospace; color: #22d3ee; opacity: .5; }\n.h-panel h3 { margin: 0; color: #f8fafc; font: 700 24px/1 system-ui; }\n.h-panel p { margin: 0; color: #94a3b8; font: 400 13px/1.5 system-ui; }\n.p1 { background: #0b1220; } .p2 { background: #0d1526; } .p3 { background: #101a2e; }\n.h-after { min-height: 320px; display: grid; place-content: center; color: #64748b; font: 400 14px system-ui; background: #0b0d12; }\n@media (prefers-reduced-motion: reduce) { .h-scene { height: auto; } .h-track { flex-direction: column; } .h-panel { flex: none; padding: 60px 40px; } }",
        "js": "gsap.registerPlugin(ScrollTrigger);\nif (!matchMedia('(prefers-reduced-motion: reduce)').matches) {\n  const track = document.querySelector('.h-track');\n  const panels = gsap.utils.toArray('.h-panel');\n  gsap.to(track, {\n    xPercent: -100 * (panels.length - 1),\n    ease: 'none',\n    scrollTrigger: {\n      trigger: '.h-scene',\n      pin: true,\n      scrub: 1,\n      invalidateOnRefresh: true,\n      end: () => '+=' + (track.scrollWidth - window.innerWidth)\n    }\n  });\n}"
      },
      "source": {
        "name": "CodePen (GreenSock) — Horizontal 'containerAnimation' ScrollTrigger (официальный демо из доков)",
        "url": "https://codepen.io/GreenSock/pen/WNjaxKp",
        "domain": "codepen.io"
      },
      "performance": "Двигается только transform трека; панели фиксированной ширины (flex), трек не пересчитывает layout. pin-spacer создаёт ScrollTrigger; не анимировать left/width — только xPercent.",
      "a11y": "prefers-reduced-motion: пин не создаётся, лента раскладывается вертикально (flex-direction: column) — контент доступен обычным скроллом.",
      "browser_support": "Везде, где работает GSAP 3 + ScrollTrigger; xPercent-движение — transform, композитный слой.",
      "ai_use": "Pin a full-height section and translate a flex track by xPercent −100×(n−1) with ease none, scrub 1 and end = track.scrollWidth − innerWidth; stack panels vertically under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 4000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-progress-bar",
      "name": "Scroll Progress Bar",
      "name_ru": "Полоса прогресса прокрутки",
      "category": "scroll",
      "tags": [
        "progress-bar",
        "reading",
        "raf",
        "vanilla-js",
        "scroll-linked"
      ],
      "summary": "Тонкий индикатор прогресса чтения: полоса сверху заполняется от 0 до 100% по мере прокрутки, процент в углу обновляется синхронно с ней.",
      "effect": "progress = scrollY / (scrollHeight − innerHeight), clamp 0..1. Полоса — scaleX на fixed-элементе (transform-origin: left), текст процента обновляется в том же кадре. Обновление только внутри rAF по пассивному scroll-событию.",
      "when_to_use": "Лонгриды, статьи блога, документация, страницы с длинным контентом; высота 3–4px, чтобы не мешать чтению.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class='readbar' aria-hidden='true'><i></i></div>\n<div class='readpct' aria-hidden='true'>0%</div>\n<article class='copy'>\n  <h1>Reading progress</h1>\n  <p>The bar is pinned to the top and scales with transform, so it never triggers layout.</p>\n  <p>Updates run once per frame via requestAnimationFrame from a passive scroll listener.</p>\n  <p>Keep scrolling to fill the bar and grow the percentage label.</p>\n</article>\n<section class='copy-end'><p>End of the article.</p></section>",
        "css": ".readbar { position: fixed; top: 0; left: 0; right: 0; height: 4px; background: #1e293b; z-index: 10; }\n.readbar i { display: block; height: 100%; background: linear-gradient(90deg, #22d3ee, #a78bfa); transform: scaleX(0); transform-origin: left; }\n.readpct { position: fixed; top: 12px; right: 12px; z-index: 10; padding: 4px 8px; border-radius: 999px; background: #111827cc; color: #7dd3fc; font: 700 11px ui-monospace, monospace; font-variant-numeric: tabular-nums; }\n.copy { padding: 46px 22px 0; }\n.copy h1 { margin: 0 0 10px; color: #f8fafc; font: 700 24px/1.15 system-ui; }\n.copy p { color: #94a3b8; font: 400 14px/1.65 system-ui; }\n.copy-end { min-height: 460px; padding: 22px; color: #475569; font: 400 13px system-ui; }\n@media (prefers-reduced-motion: reduce) { .readbar i { background: #22d3ee; } }",
        "js": "const bar = document.querySelector('.readbar i');\nconst pct = document.querySelector('.readpct');\nlet ticking = false;\nconst update = () => {\n  ticking = false;\n  const max = document.documentElement.scrollHeight - window.innerHeight;\n  const p = max > 0 ? Math.min(window.scrollY / max, 1) : 0;\n  bar.style.transform = `scaleX(${p})`;\n  pct.textContent = Math.round(p * 100) + '%';\n};\naddEventListener('scroll', () => {\n  if (!ticking) { ticking = true; requestAnimationFrame(update); }\n}, { passive: true });\nupdate();"
      },
      "source": {
        "name": "CSS-Tricks — Reading Position Indicator",
        "url": "https://css-tricks.com/reading-position-indicator/",
        "domain": "css-tricks.com"
      },
      "performance": "Compositor-only (scaleX), passive listener + rAF-троттлинг, одно чтение размеров за кадр; никаких style/layout-чтений внутри scroll-обработчика.",
      "a11y": "Индикатор декоративный (aria-hidden) — скринридер не озвучивает проценты на каждый кадр; при prefers-reduced-motion полоса становится статичного цвета.",
      "browser_support": "Все современные браузеры (scrollHeight/scrollY, rAF, passive listeners — baseline).",
      "ai_use": "Add a 4px fixed reading-progress bar: passive scroll listener → rAF → progress = scrollY/(scrollHeight−innerHeight), write scaleX on the fill and textContent percent; mark aria-hidden.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-css-scroll-timeline",
      "name": "Pure CSS Scroll-Driven Animation",
      "name_ru": "Скролл-анимация на чистом CSS",
      "category": "scroll",
      "tags": [
        "css",
        "animation-timeline",
        "scroll-timeline",
        "no-js",
        "zero-javascript"
      ],
      "summary": "Скролл-анимации вообще без JavaScript: солнце поднимается, бейдж вращается, фоновый градиент меняет оттенок — всё через animation-timeline: scroll(root).",
      "effect": "animation-timeline: scroll(root block) жёстко привязывает ключевые кадры к позиции скролла: солнце едет translateY 0→−260px и меняет opacity .4→1, бейдж крутится 0→360°, фон плавно получает hue-rotate(40deg). Длительность не задаётся — прогресс линейный и обратимый: скроллишь вверх — всё едет назад.",
      "when_to_use": "Декор hero, фоновые слои, прогресс-индикаторы чтения — везде, где хочется zero-JS и анимацию вне main thread.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class='scene' aria-hidden='true'>\n  <div class='scene-sun'></div>\n  <div class='scene-badge'>scroll()</div>\n</div>\n<main class='doc'>\n  <section><h1>Zero JavaScript</h1><p>The sun tracks scroll progress via animation-timeline: scroll(root).</p></section>\n  <section><h2>One property</h2><p>No listeners, no observers — the browser drives the keyframes off the scrollport.</p></section>\n  <section><h2>Graceful fallback</h2><p>Without support nothing animates — the page still reads fine.</p></section>\n</main>",
        "css": ".scene { position: fixed; inset: 0; pointer-events: none; background: linear-gradient(#0a2540, #0b0d12 80%); }\n.scene-sun { position: absolute; left: 50%; top: 220px; width: 64px; height: 64px; margin-left: -32px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #fde68a, #f59e0b); }\n.scene-badge { position: absolute; left: 50%; top: 120px; margin-left: -46px; width: 92px; text-align: center; padding: 6px 0; border: 1px solid #38bdf855; border-radius: 999px; color: #7dd3fc; font: 700 11px ui-monospace, monospace; }\n.doc { position: relative; z-index: 1; }\n.doc section { min-height: 100vh; box-sizing: border-box; padding: 46px 24px 0; }\n.doc h1, .doc h2 { margin: 0 0 8px; color: #f8fafc; font: 700 22px/1.2 system-ui; }\n.doc p { margin: 0; color: #94a3b8; font: 400 14px/1.6 system-ui; }\n@supports (animation-timeline: scroll()) {\n  .scene-sun { animation: rise linear both; animation-timeline: scroll(root block); }\n  .scene-badge { animation: spin linear both; animation-timeline: scroll(root block); }\n  .scene { animation: glow linear both; animation-timeline: scroll(root block); }\n}\n@keyframes rise { from { transform: translateY(0); opacity: .4; } to { transform: translateY(-260px); opacity: 1; } }\n@keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }\n@keyframes glow { to { filter: hue-rotate(40deg); } }\n@media (prefers-reduced-motion: reduce) { .scene-sun, .scene-badge, .scene { animation: none !important; } }",
        "js": ""
      },
      "source": {
        "name": "MDN — Scroll-driven animation timelines (scroll() / view())",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations/Timelines",
        "domain": "developer.mozilla.org"
      },
      "performance": "В Chrome/Edge и Safari scroll-driven анимации выполняются на композиторе вне main thread; в неподдерживающих браузерах без @supports страница просто статична — бесплатная деградация.",
      "a11y": "prefers-reduced-motion: animation: none !important для солнца, бейджа и фона; контент читается без анимации, @supports-фолбэк всегда оставляет статичную читаемую страницу.",
      "browser_support": "Chrome/Edge 115+, Safari 26+, Firefox — только за флагом layout.css.scroll-driven-animations.enabled (без поддержки анимация не запускается и не ломает вид).",
      "ai_use": "For decorative scroll effects with zero JS use @supports (animation-timeline: scroll()) and animate transform/filter with animation-timeline: scroll(root block); disable with animation: none under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3500,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-velocity-skew",
      "name": "Scroll Velocity Skew",
      "name_ru": "Скос по скорости прокрутки",
      "category": "scroll",
      "tags": [
        "gsap",
        "scrolltrigger",
        "velocity",
        "skew",
        "smooth"
      ],
      "summary": "Скос по скорости прокрутки: пока скролл быстрый, строки списка наклоняются по skewY, а после остановки плавно выпрямляются за 0.8s с easing power3.",
      "effect": "ScrollTrigger.create({ start: 0, end: 'max' }) на каждом onUpdate читает self.getVelocity(); skew = clamp(velocity / −320, −12°..12°). Новый скос применяется только если он резче текущего, после чего прокси-значение твинится к нулю (0.8s, power3), а quickSetter пишет skewY каждый кадр. transform-origin: right center.",
      "when_to_use": "Списки, текстовые ленты, галереи работ в стиле Lenis/GSAP-сайтов; финальная полировка после включения smooth-scroll.",
      "tech": [
        "gsap"
      ],
      "dependencies": [
        "gsap 3.13 + ScrollTrigger (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js'></script>\n<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js'></script>\n<div class='vel-head'><h1>Velocity skew</h1><p>Scroll fast — the rows lean, then settle back.</p></div>\n<div class='vel-list'>\n  <div class='vel-row skewElem'><span>Motion</span><i class='g1'></i></div>\n  <div class='vel-row skewElem'><span>Velocity</span><i class='g2'></i></div>\n  <div class='vel-row skewElem'><span>Momentum</span><i class='g3'></i></div>\n  <div class='vel-row skewElem'><span>Friction</span><i class='g4'></i></div>\n</div>\n<div class='vel-foot'><p>Settle time is 0.8s with power3 easing.</p></div>",
        "css": ".vel-head { padding: 34px 22px 10px; background: #0b0d12; }\n.vel-head h1 { margin: 0 0 6px; color: #f8fafc; font: 700 24px system-ui; }\n.vel-head p { margin: 0; color: #94a3b8; font: 400 13px system-ui; }\n.vel-list { padding: 14px 22px; display: grid; gap: 12px; background: #0b0d12; }\n.vel-row { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-radius: 12px; background: #111827; border: 1px solid #1e293b; transform-origin: right center; will-change: transform; }\n.vel-row span { color: #e2e8f0; font: 700 18px system-ui; }\n.vel-row i { width: 70px; height: 8px; border-radius: 999px; }\n.g1 { background: #22d3ee; } .g2 { background: #a78bfa; } .g3 { background: #34d399; } .g4 { background: #f59e0b; }\n.vel-foot { min-height: 340px; padding: 22px; color: #64748b; font: 400 13px system-ui; background: #0b0d12; }\n@media (prefers-reduced-motion: reduce) { .vel-row { transform: none !important; will-change: auto; } }",
        "js": "gsap.registerPlugin(ScrollTrigger);\nif (!matchMedia('(prefers-reduced-motion: reduce)').matches) {\n  const proxy = { skew: 0 };\n  const setSkew = gsap.quickSetter('.vel-row', 'skewY', 'deg');\n  const clamp = gsap.utils.clamp(-12, 12);\n  gsap.set('.vel-row', { transformOrigin: 'right center', force3D: true });\n  ScrollTrigger.create({\n    start: 0,\n    end: 'max',\n    onUpdate: (self) => {\n      const skew = clamp(self.getVelocity() / -320);\n      if (Math.abs(skew) > Math.abs(proxy.skew)) {\n        proxy.skew = skew;\n        gsap.to(proxy, {\n          skew: 0, duration: 0.8, ease: 'power3', overwrite: true,\n          onUpdate: () => setSkew(proxy.skew)\n        });\n      }\n    }\n  });\n}"
      },
      "source": {
        "name": "CodePen (GreenSock) — Skew on scroll using scroll velocity",
        "url": "https://codepen.io/GreenSock/pen/eYpGLYL",
        "domain": "codepen.io"
      },
      "performance": "quickSetter — прямая запись transform без создания твинов на каждый кадр; skew на 4–8 элементах остаётся композитным. Не применять skew к большим текстовым полотнам — растеризация смазывает текст.",
      "a11y": "При prefers-reduced-motion эффект не инициализируется, а transform принудительно сброшен через media query.",
      "browser_support": "Везде, где работает GSAP 3 + ScrollTrigger (Chrome 90+, Safari 15+, Firefox 90+).",
      "ai_use": "Skew list rows on scroll speed: read self.getVelocity() in ScrollTrigger onUpdate, clamp velocity/−320 to ±12°, ease back to 0 over 0.8s power3 via a proxy + quickSetter; skip under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 4000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-zoom-hero",
      "name": "Zoom-on-Scroll Hero",
      "name_ru": "Зум hero-медиа при прокрутке",
      "category": "scroll",
      "tags": [
        "gsap",
        "scrolltrigger",
        "zoom",
        "scale",
        "scrub",
        "hero"
      ],
      "summary": "Hero-«окно» с зумом на скролле: медиа-рамка пинится и плавно масштабируется от 1 до 1.6, пока страница прокручивается, затем пин отпускает секцию дальше.",
      "effect": "fromTo scale 1 → 1.6 (и border-radius 16px → 0) с ease: none, ScrollTrigger pin: true, end '+=700', scrub: 1 — масштаб жёстко привязан к позиции скролла без собственной длительности и обратим.",
      "when_to_use": "Hero с крупным изображением или 3D-кадром, промо продукта, «влёт в сцену» перед основным контентом; фокус-элемент на первом экране.",
      "tech": [
        "gsap"
      ],
      "dependencies": [
        "gsap 3.13 + ScrollTrigger (CDN)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js'></script>\n<script src='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js'></script>\n<section class='zoom-scene'>\n  <div class='zoom-media'>\n    <svg viewBox='0 0 480 320' preserveAspectRatio='xMidYMid slice'>\n      <rect width='480' height='320' fill='#0d1b2a'/>\n      <circle cx='360' cy='70' r='46' fill='#f59e0b'/>\n      <path d='M0 250 120 150l90 70 80-110 90 120 100-70v160H0z' fill='#132c3f'/>\n      <path d='M0 290 150 210l120 60 110-70 100 70v50H0z' fill='#0a1a28'/>\n    </svg>\n    <div class='zoom-caption'><h1>Into the valley</h1><p>Scroll to zoom the frame.</p></div>\n  </div>\n</section>\n<section class='zoom-after'><p>Scale is scrubbed from 1 to 1.6 over the pinned hero.</p></section>",
        "css": ".zoom-scene { height: 100vh; display: grid; place-items: center; background: #0b0d12; overflow: hidden; }\n.zoom-media { position: relative; width: 78%; height: 72%; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 50px #000a; }\n.zoom-media svg { width: 100%; height: 100%; display: block; }\n.zoom-caption { position: absolute; left: 16px; bottom: 14px; }\n.zoom-caption h1 { margin: 0 0 4px; color: #f8fafc; font: 700 22px/1 system-ui; }\n.zoom-caption p { margin: 0; color: #cbd5e1; font: 400 12px system-ui; }\n.zoom-after { min-height: 340px; display: grid; place-content: center; color: #64748b; font: 400 14px system-ui; background: #0b0d12; }\n@media (prefers-reduced-motion: reduce) { .zoom-scene { height: auto; padding: 40px 0; } }",
        "js": "gsap.registerPlugin(ScrollTrigger);\nif (!matchMedia('(prefers-reduced-motion: reduce)').matches) {\n  gsap.fromTo('.zoom-media',\n    { scale: 1, borderRadius: 16 },\n    {\n      scale: 1.6,\n      borderRadius: 0,\n      ease: 'none',\n      scrollTrigger: { trigger: '.zoom-scene', start: 'top top', end: '+=700', pin: true, scrub: 1 }\n    });\n}"
      },
      "source": {
        "name": "Codrops — Building a Layered Zoom Scroll Effect with GSAP ScrollTrigger",
        "url": "https://tympanus.net/codrops/2025/10/29/building-a-layered-zoom-scroll-effect-with-gsap-scrollsmoother-and-scrolltrigger/",
        "domain": "tympanus.net"
      },
      "performance": "Scale — compositor-only, но исходное медиа должно быть в 1.5–2× разрешении, иначе на пике зума будет мыло; pin добавляет служебный spacer, не вкладывать filter в zoom-слой.",
      "a11y": "prefers-reduced-motion: анимация и пин отключены, секция обычной высоты, медиа показано в масштабе 1:1 без движения.",
      "browser_support": "Везде, где работает GSAP 3 + ScrollTrigger (Chrome 90+, Safari 15+, Firefox 90+).",
      "ai_use": "Pin a hero and scrub-scale its media from 1 to 1.6 with ease none over +=700px; no pin/scale under prefers-reduced-motion, show media at rest.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 4000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-snap-sections",
      "name": "Scroll-Snap Sections",
      "name_ru": "Секции со снапом прокрутки",
      "category": "scroll",
      "tags": [
        "scroll-snap",
        "css-only",
        "sections",
        "carousel",
        "native-scroll"
      ],
      "summary": "Секции страницы со снапом: прокрутка мягко останавливается на границах экранов, а в средней секции — горизонтальная лента карточек с прилипанием по центру.",
      "effect": "html { scroll-snap-type: y proximity } + scroll-snap-align: start на секциях высотой 100vh; внутренняя лента — x mandatory со scroll-snap-align: center. Анимирует сам браузер нативной физикой, JS в кадре не участвует; proximity не запирает контент, mandatory — только для коротких секций.",
      "when_to_use": "Презентационные одностраничники, полноэкранные сториз-секции, галереи-карусели; для лонгридов всегда proximity, mandatory — только на коротких одноэкранных секциях.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<main class='snap'>\n  <section class='pane a'><h1>Snap sections</h1><p>Page-level scroll-snap-type: y proximity.</p></section>\n  <section class='pane b'>\n    <h2>Horizontal strip</h2>\n    <div class='strip'><i>01</i><i>02</i><i>03</i><i>04</i></div>\n  </section>\n  <section class='pane c'><h2>End of snap</h2><p>Native scrolling, no scroll-jacking.</p></section>\n</main>",
        "css": "html { scroll-snap-type: y proximity; }\n.snap { background: #0b0d12; }\n.pane { min-height: 100vh; box-sizing: border-box; display: grid; place-content: center; gap: 10px; padding: 30px 24px; scroll-snap-align: start; }\n.pane h1, .pane h2 { margin: 0; color: #f8fafc; font: 700 22px/1.2 system-ui; text-align: center; }\n.pane p { margin: 0; color: #94a3b8; font: 400 13px/1.5 system-ui; text-align: center; }\n.pane.b { gap: 18px; }\n.strip { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory; padding: 4px; max-width: 420px; }\n.strip i { flex: 0 0 130px; height: 96px; display: grid; place-items: center; border-radius: 12px; scroll-snap-align: center; background: #132033; color: #7dd3fc; font: 700 16px ui-monospace, monospace; font-style: normal; }\n.strip i:nth-child(2) { background: #1b2a44; }\n.strip i:nth-child(3) { background: #22304d; }\n.strip i:nth-child(4) { background: #293a5c; }\n@media (prefers-reduced-motion: reduce) { html { scroll-snap-type: none; } }",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks — Practical CSS Scroll Snapping",
        "url": "https://css-tricks.com/practical-css-scroll-snapping/",
        "domain": "css-tricks.com"
      },
      "performance": "Снап работает в нативном скролле, JS на каждый кадр не выполняется; mandatory может раздражать на высоких секциях — использовать осознанно, для длинного контента proximity.",
      "a11y": "prefers-reduced-motion отключает снап (scroll-snap-type: none), оставляя обычную прокрутку; клавиатура, тач и скринридеры работают нативно.",
      "browser_support": "scroll-snap: Chrome 69+, Safari 11+, Firefox 68+, Edge 79+ (baseline).",
      "ai_use": "For full-screen scrolling sections set html { scroll-snap-type: y proximity } with scroll-snap-align: start on 100vh panes, and x mandatory inside horizontal galleries; disable snapping under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3500,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-sticky-stack",
      "name": "Sticky Stacked Cards",
      "name_ru": "Липкая колода карточек",
      "category": "scroll",
      "tags": [
        "sticky",
        "stacking",
        "animation-timeline",
        "view-timeline",
        "cards"
      ],
      "summary": "Колода карточек: каждая прилипает к верху со своим отступом, следующая наезжает сверху, а предыдущие плавно уменьшаются по мере выхода из вьюпорта — всё на CSS, без JS.",
      "effect": "position: sticky + top: calc(8px + i*34px); масштаб уменьшается от 1.0 до 0.7 через animation-timeline: --deck (view-timeline на колоде) и animation-range: exit-crossing (i/4 → (i+1)/4): каждая карточка уходит в глубину в своей четверти выхода.",
      "when_to_use": "Списки возможностей, тарифы, кейсы, отзывы — всё, что выигрывает от «стопки» с ощущением глубины и поочерёдным прочтением.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<header class='deck-head'><h1>Stacked cards</h1><p>Each card sticks and scales down under the next one.</p></header>\n<div class='deck'>\n  <article class='deck-card' style='--i:0'><div class='deck-inner'><span>01</span><h3>Scope</h3><p>One week of discovery.</p></div></article>\n  <article class='deck-card' style='--i:1'><div class='deck-inner'><span>02</span><h3>Design</h3><p>Two directions, one pick.</p></div></article>\n  <article class='deck-card' style='--i:2'><div class='deck-inner'><span>03</span><h3>Build</h3><p>Ship in two sprints.</p></div></article>\n  <article class='deck-card' style='--i:3'><div class='deck-inner'><span>04</span><h3>Scale</h3><p>Measure and iterate.</p></div></article>\n</div>\n<footer class='deck-foot'><p>Pure CSS: sticky + view() timeline.</p></footer>",
        "css": ".deck-head { padding: 30px 24px 14px; background: #0b0d12; }\n.deck-head h1 { margin: 0 0 6px; color: #f8fafc; font: 700 22px/1.1 system-ui; }\n.deck-head p { margin: 0; color: #94a3b8; font: 400 13px/1.5 system-ui; }\n.deck { --n: 4; view-timeline-name: --deck; display: grid; gap: 8px; padding: 0 20px 60vh; background: #0b0d12; }\n.deck-card { position: sticky; top: calc(8px + var(--i) * 34px); }\n.deck-inner { display: grid; gap: 4px; padding: 16px 18px; border-radius: 14px; background: linear-gradient(150deg, #14243a, #0f172a); border: 1px solid #2b3b55; box-shadow: 0 14px 30px #0008; }\n.deck-inner span { color: #22d3ee; font: 700 12px ui-monospace, monospace; }\n.deck-inner h3 { margin: 0; color: #f1f5f9; font: 700 18px/1 system-ui; }\n.deck-inner p { margin: 0; color: #94a3b8; font: 400 12px/1.4 system-ui; }\n.deck-foot { min-height: 220px; display: grid; place-content: center; color: #64748b; font: 400 13px system-ui; background: #0b0d12; }\n@supports (animation-timeline: view()) {\n  .deck-inner {\n    --start: calc(var(--i) / var(--n) * 100%);\n    --end: calc((var(--i) + 1) / var(--n) * 100%);\n    animation: deck-scale linear both;\n    animation-timeline: --deck;\n    animation-range: exit-crossing var(--start) exit-crossing var(--end);\n  }\n}\n@keyframes deck-scale { to { transform: scale(calc(1.1 - 0.1 * (var(--n) - var(--i)))); } }\n@media (prefers-reduced-motion: reduce) { .deck-inner { animation: none !important; } }",
        "js": ""
      },
      "source": {
        "name": "scroll-driven-animations.style (Bramus) — Stacking Cards (CSS)",
        "url": "https://scroll-driven-animations.style/demos/stacking-cards/css/",
        "domain": "scroll-driven-animations.style"
      },
      "performance": "Sticky + view() без JS на кадр; анимируется transform внутренней карточки, высота колоды не меняется. Не вешать filter на sticky-элементы — ломает композитинг.",
      "a11y": "prefers-reduced-motion: анимация масштаба отключена, карточки просто аккуратно стоят друг под другом; sticky не создаёт движения и не блокирует скролл.",
      "browser_support": "Chrome/Edge 115+, Safari 26+, Firefox — за флагом; без поддержки колода отображается статичной стопкой.",
      "ai_use": "Stack 4 cards with position: sticky and increasing top offsets; scale each inner card down via animation-timeline: view() named on the deck with exit-crossing ranges per index; no animation under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 4000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-curtain-reveal",
      "name": "Curtain Reveal on Scroll",
      "name_ru": "Занавес-раскрытие при прокрутке",
      "category": "scroll",
      "tags": [
        "curtain",
        "sticky",
        "reveal",
        "translate3d",
        "vanilla-js"
      ],
      "summary": "Занавес на скролле: две глухие панели закрывают экран, и по мере прокрутки разъезжаются влево и вправо, открывая контент за ними; скроллишь вверх — занавес закрывается.",
      "effect": "Секция высотой 200vh, внутри sticky-сцена 100vh; прогресс = −section.top / (height − 100vh), панели едут translate3d(±101% · p, 0, 0). Расчёт в одном rAF, движение линейно связано со скроллом и полностью обратимо.",
      "when_to_use": "Переход между главами лендинга, смена цветовой темы, интро к следующему блоку; 1–2 занавеса на страницу, чтобы не утомлять.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class='curtain'>\n  <div class='curtain-stage'>\n    <div class='curtain-behind'>\n      <p class='curtain-kicker'>Revealed</p>\n      <h2>What is behind<br>the curtain</h2>\n    </div>\n    <div class='curtain-panel left'><span>OPEN</span></div>\n    <div class='curtain-panel right'><span>SESAME</span></div>\n  </div>\n</section>\n<section class='curtain-after'><p>Scroll back up — the curtain closes again.</p></section>",
        "css": ".curtain { position: relative; height: 200vh; background: #0b0d12; }\n.curtain-stage { position: sticky; top: 0; height: 100vh; overflow: hidden; }\n.curtain-behind { position: absolute; inset: 0; display: grid; place-content: center; gap: 8px; text-align: center; background: linear-gradient(140deg, #10243b, #0b0d12 75%); }\n.curtain-behind h2 { margin: 0; color: #f8fafc; font: 700 26px/1.15 system-ui; }\n.curtain-kicker { margin: 0; color: #7dd3fc; font: 700 11px ui-monospace, monospace; letter-spacing: .18em; text-transform: uppercase; }\n.curtain-panel { position: absolute; top: 0; bottom: 0; width: 51%; display: grid; place-items: center; background: linear-gradient(180deg, #1e293b, #111827); border-inline: 1px solid #334155; will-change: transform; }\n.curtain-panel span { color: #475569; font: 700 16px ui-monospace, monospace; letter-spacing: .4em; }\n.curtain-panel.left { left: 0; }\n.curtain-panel.right { right: 0; }\n.curtain-after { min-height: 300px; display: grid; place-content: center; color: #64748b; font: 400 14px system-ui; background: #0b0d12; }\n@media (prefers-reduced-motion: reduce) { .curtain-panel { display: none; } }",
        "js": "const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst section = document.querySelector('.curtain');\nconst left = document.querySelector('.curtain-panel.left');\nconst right = document.querySelector('.curtain-panel.right');\nif (!reduce && section) {\n  let ticking = false;\n  const update = () => {\n    ticking = false;\n    const span = section.offsetHeight - window.innerHeight;\n    const p = span > 0 ? Math.min(Math.max(-section.getBoundingClientRect().top / span, 0), 1) : 0;\n    left.style.transform = `translate3d(${-101 * p}%, 0, 0)`;\n    right.style.transform = `translate3d(${101 * p}%, 0, 0)`;\n  };\n  addEventListener('scroll', () => {\n    if (!ticking) { ticking = true; requestAnimationFrame(update); }\n  }, { passive: true });\n  update();\n}"
      },
      "source": {
        "name": "CSS-Tricks — How to Make a 'Raise the Curtains' Effect in CSS",
        "url": "https://css-tricks.com/css-raise-the-curtains-effect/",
        "domain": "css-tricks.com"
      },
      "performance": "Только translate3d панелей — compositor; расчёт прогресса в одном rAF на кадр; overflow: hidden на сцене обязателен, иначе панели видны до раскрытия.",
      "a11y": "prefers-reduced-motion: панели скрыты (display: none), контент сразу виден; никакого scroll-jacking — скролл остаётся нативным.",
      "browser_support": "sticky + translate3d: все современные браузеры (Chrome, Safari, Firefox, Edge — baseline).",
      "ai_use": "Create a 200vh section with a sticky 100vh stage: two 51% panels translate3d ±101% × progress as the section scrolls, revealing content behind; none of it under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 4000,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "scroll-dial-counter",
      "name": "Scroll-Linked Dial & Counter",
      "name_ru": "Циферблат и счётчик по прогрессу",
      "category": "scroll",
      "tags": [
        "dial",
        "counter",
        "scroll-linked",
        "vanilla-js",
        "indicator",
        "conic-gradient"
      ],
      "summary": "Скролл-индикатор в виде циферблата: стрелка поворачивается от −120° до +120°, кольцо заполняется conic-gradient, а цифра считает от 0% до 100% по мере прокрутки.",
      "effect": "p = scrollY / (scrollHeight − innerHeight); стрелка rotate(−120 + p·240deg), кольцо — CSS-переменная --p внутри conic-gradient(calc(var(--p) * 1turn), …), число обновляется textContent. Один rAF на кадр, значения с tabular-nums — цифры не дёргаются.",
      "when_to_use": "Метрики проекта, «сколько осталось до конца», прогресс квиза или главы; визуальная альтернатива полосе прогресса, когда нужен акцент-цифра.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class='dial-wrap'>\n  <div class='dial' id='dial'>\n    <div class='dial-ring'></div>\n    <div class='dial-needle'></div>\n    <div class='dial-hub'></div>\n    <output class='dial-num' id='dialNum'>0%</output>\n  </div>\n  <p class='dial-caption'>Position in page</p>\n</div>\n<section class='dial-tall'><h1>Scroll-linked indicator</h1><p>Needle, ring and number all read the same scroll progress.</p></section>\n<section class='dial-foot'><p>Done — the dial rests at 100%.</p></section>",
        "css": ".dial-wrap { display: grid; justify-items: center; gap: 8px; padding: 34px 0 10px; background: #0b0d12; }\n.dial { position: relative; width: 132px; height: 132px; border-radius: 50%; background: #111827; }\n.dial-ring { position: absolute; inset: 6px; border-radius: 50%; background: conic-gradient(#22d3ee calc(var(--p, 0) * 1turn), #1f2b3d 0); -webkit-mask: radial-gradient(circle, transparent 52%, #000 53%); mask: radial-gradient(circle, transparent 52%, #000 53%); }\n.dial-needle { position: absolute; left: 50%; top: 50%; width: 3px; height: 48px; margin-left: -1.5px; margin-top: -48px; border-radius: 3px; background: #f8fafc; transform-origin: 50% 100%; transform: rotate(-120deg); }\n.dial-hub { position: absolute; left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; background: #22d3ee; }\n.dial-num { position: absolute; left: 50%; top: 62%; transform: translateX(-50%); color: #f8fafc; font: 700 20px ui-monospace, monospace; font-variant-numeric: tabular-nums; }\n.dial-caption { margin: 0; color: #64748b; font: 400 12px system-ui; }\n.dial-tall { min-height: 100vh; box-sizing: border-box; display: grid; place-content: center; gap: 8px; padding: 0 26px; text-align: center; background: #0b0d12; }\n.dial-tall h1 { margin: 0; color: #f8fafc; font: 700 22px/1.2 system-ui; }\n.dial-tall p { margin: 0; color: #94a3b8; font: 400 13px/1.55 system-ui; }\n.dial-foot { min-height: 300px; display: grid; place-content: center; color: #475569; font: 400 13px system-ui; background: #0b0d12; }\n@media (prefers-reduced-motion: reduce) { .dial-needle { transform: rotate(-120deg) !important; } }",
        "js": "const dial = document.getElementById('dial');\nconst needle = dial.querySelector('.dial-needle');\nconst num = document.getElementById('dialNum');\nconst reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\nlet ticking = false;\nconst update = () => {\n  ticking = false;\n  const max = document.documentElement.scrollHeight - window.innerHeight;\n  const p = max > 0 ? Math.min(window.scrollY / max, 1) : 0;\n  num.textContent = Math.round(p * 100) + '%';\n  if (!reduce) {\n    needle.style.transform = `rotate(${-120 + p * 240}deg)`;\n    dial.style.setProperty('--p', p.toFixed(4));\n  }\n};\naddEventListener('scroll', () => {\n  if (!ticking) { ticking = true; requestAnimationFrame(update); }\n}, { passive: true });\nupdate();"
      },
      "source": {
        "name": "CSS-Tricks — Animating Number Counters (@property + counter)",
        "url": "https://css-tricks.com/animating-number-counters/",
        "domain": "css-tricks.com"
      },
      "performance": "Небольшой conic-gradient (132px) перерисовывается при смене --p — дешёвый paint; стрелка — transform; число обновляется textContent с tabular-nums, одно чтение размеров за кадр.",
      "a11y": "prefers-reduced-motion: стрелка фиксируется на −120°, остаётся текстовый процент; число не является live-region, поэтому скринридер не получает поток «31%, 32%, 33%».",
      "browser_support": "conic-gradient и CSS mask: Chrome 90+, Safari 15.4+, Firefox 89+, Edge 90+.",
      "ai_use": "Drive a dial indicator from scroll progress: needle rotate(−120 + p×240deg), conic-gradient ring via --p, percent counter with tabular-nums, all updated once per rAF; freeze needle under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3500,
        "bg": "#0b0d12"
      },
      "_file": "agent-03-scroll.json"
    },
    {
      "id": "hover-magnetic-button",
      "name": "Magnetic Button",
      "name_ru": "Магнитная кнопка",
      "category": "hover",
      "tags": [
        "hover",
        "cursor-tracking",
        "transform",
        "micro-interaction",
        "vanilla-js"
      ],
      "summary": "Кнопка слегка «притягивается» к курсору: сдвигается на 30% смещения от центра и упруго возвращается на место, когда мышь уходит.",
      "effect": "На pointermove считаем смещение курсора от центра кнопки и сдвигаем её через translate (30% смещения), текст внутри — на 40% этого сдвига для параллакса; transition 220ms cubic-bezier(.22,.68,.28,1). На pointerleave — возврат пружиной cubic-bezier(.34,1.56,.64,1) за 550ms. В демо раз в 2.6s срабатывает авто-цикл притяжения, чтобы эффект было видно и без движения мыши.",
      "when_to_use": "Крупные CTA: hero, кнопка «Оставить заявку», финальные блоки лендинга.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <button class=\"magnetic\" type=\"button\"><span>Get started</span></button>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.magnetic{position:relative;padding:16px 38px;border:0;border-radius:999px;background:#2f6bff;color:#fff;font-size:16px;font-weight:600;cursor:pointer;transition:transform .22s cubic-bezier(.22,.68,.28,1)}\n.magnetic span{display:inline-block;transition:transform .22s cubic-bezier(.22,.68,.28,1)}\n@media (prefers-reduced-motion:reduce){.magnetic,.magnetic span{transition:none}}",
        "js": "const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst el = document.querySelector('.magnetic');\nconst label = el.querySelector('span');\nlet hovering = false;\nconst place = (x, y) => {\n  el.style.transform = `translate(${x}px, ${y}px)`;\n  label.style.transform = `translate(${x * .4}px, ${y * .4}px)`;\n};\nif (!reduced && el) {\n  el.addEventListener('pointerenter', () => { hovering = true; });\n  el.addEventListener('pointermove', (e) => {\n    const r = el.getBoundingClientRect();\n    place((e.clientX - r.left - r.width / 2) * .3, (e.clientY - r.top - r.height / 2) * .3);\n  });\n  el.addEventListener('pointerleave', () => {\n    hovering = false;\n    el.style.transition = label.style.transition = 'transform .55s cubic-bezier(.34,1.56,.64,1)';\n    place(0, 0);\n    setTimeout(() => { el.style.transition = label.style.transition = ''; }, 600);\n  });\n  setInterval(() => {\n    if (hovering) return;\n    el.style.transition = label.style.transition = 'transform .45s cubic-bezier(.34,1.56,.64,1)';\n    place(30, -14);\n    setTimeout(() => place(0, 0), 850);\n  }, 2600);\n}"
      },
      "source": {
        "name": "Codrops — Magnetic Buttons",
        "url": "https://tympanus.net/codrops/2020/08/05/magnetic-buttons/",
        "domain": "tympanus.net"
      },
      "performance": "Compositor-only (transform). getBoundingClientRect на каждый pointermove — ок для одной кнопки; для списков кэшировать rect на pointerenter.",
      "a11y": "prefers-reduced-motion:reduce — JS-логика не активируется, кнопка остаётся обычной, фокус не затрагивается.",
      "browser_support": "Chrome 88+, Safari 14+, Firefox 85+ (pointer events); сам приём работает везде, где есть CSS transitions.",
      "ai_use": "Apply to primary CTA: on pointermove translate the button by 30% of the cursor offset from its center (label by 40% of that for parallax), return with a spring cubic-bezier(.34,1.56,.64,1) 550ms on pointerleave; skip all listeners under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-cursor-follower",
      "name": "Cursor Follower (Dot + Lag Ring)",
      "name_ru": "Курсор-компаньон: точка и догоняющее кольцо",
      "category": "hover",
      "tags": [
        "hover",
        "custom-cursor",
        "lerp",
        "requestanimationframe",
        "vanilla-js"
      ],
      "summary": "Кастомный курсор из двух элементов: точка следует за мышью мгновенно, а кольцо плавно догоняет её с запаздыванием.",
      "effect": "Точка позиционируется через translate3d сразу на pointermove; кольцо интерполируется к цели (lerp 0.16 за кадр requestAnimationFrame), создавая lag-эффект. При наведении на ссылки/кнопки кольцо увеличивается до scale 1.6 и меняет цвет на #22d3ee за 250ms.",
      "when_to_use": "Портфолио, промо-лендинги, творческие студии; только для устройств с мышью.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <button class=\"pill\" type=\"button\">Hover the button</button>\n  <div class=\"cur-dot\" aria-hidden=\"true\"></div>\n  <div class=\"cur-ring\" aria-hidden=\"true\"></div>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif;cursor:none}\n.pill{padding:14px 28px;border-radius:999px;border:1px solid #3a435a;background:#151a26;color:#e8ecf3;font-size:15px;cursor:none}\n.cur-dot,.cur-ring{position:fixed;top:0;left:0;pointer-events:none;border-radius:50%;translate:-50% -50%}\n.cur-dot{width:8px;height:8px;background:#e8ecf3}\n.cur-ring{width:36px;height:36px;border:1.5px solid #4f7cff;transition:scale .25s cubic-bezier(.34,1.56,.64,1),border-color .25s}\n.cur-ring.is-active{scale:1.6;border-color:#22d3ee}\n@media (prefers-reduced-motion:reduce){.cur-dot,.cur-ring{display:none}#demo{cursor:auto}}",
        "js": "const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst dot = document.querySelector('.cur-dot');\nconst ring = document.querySelector('.cur-ring');\nif (!reduced && dot && ring) {\n  let x = innerWidth / 2, y = innerHeight / 2, rx = x, ry = y;\n  const place = (node, px, py) => { node.style.transform = `translate3d(${px}px,${py}px,0)`; };\n  place(dot, x, y);\n  addEventListener('pointermove', (e) => {\n    x = e.clientX; y = e.clientY;\n    place(dot, x, y);\n    ring.classList.toggle('is-active', !!e.target.closest('a,button'));\n  });\n  (function frame() {\n    rx += (x - rx) * .16;\n    ry += (y - ry) * .16;\n    place(ring, rx, ry);\n    requestAnimationFrame(frame);\n  })();\n}"
      },
      "source": {
        "name": "CSS-Tricks — Next Level CSS Styling for Cursors",
        "url": "https://css-tricks.com/next-level-css-styling-for-cursors/",
        "domain": "css-tricks.com"
      },
      "performance": "Compositor-only: в rAF-цикле меняется только transform. Не создавать на touch-устройствах.",
      "a11y": "prefers-reduced-motion:reduce — оба элемента скрыты (display:none), возвращается системный курсор; элементы aria-hidden и pointer-events:none.",
      "browser_support": "Chrome 104+, Safari 14.1+, Firefox 72+ (свойства translate/scale); pointer events — Chrome 55+, Safari 13+, Firefox 59+.",
      "ai_use": "Add two aria-hidden follower divs: dot snaps to the pointer via translate3d on pointermove, ring lerps to it at 0.16 per rAF frame and scales to 1.6 while hovering links/buttons; hide both and restore the native cursor under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-cursor-blend-difference",
      "name": "Cursor Invert with mix-blend-mode: difference",
      "name_ru": "Курсор-инвертор через difference",
      "category": "hover",
      "tags": [
        "hover",
        "custom-cursor",
        "mix-blend-mode",
        "invert",
        "vanilla-js"
      ],
      "summary": "Белый круг-курсор с mix-blend-mode: difference инвертирует всё под собой: над тёмным фоном он светлый, над белым текстом — тёмный.",
      "effect": "Круг 64px следует за pointermove через translate3d с лёгким запаздыванием (lerp 0.2 за кадр, rAF). Режим difference вычитает цвета круга и фона, поэтому типографика и фон «проявляются» в негативе без всяких масок.",
      "when_to_use": "Тёмные hero-секции с крупной типографикой, креативные студии, промо-страницы.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <h1 class=\"blend-title\">MIX<br>BLEND</h1>\n  <p class=\"blend-note\">Move your cursor to invert</p>\n  <div class=\"blend-cursor\" aria-hidden=\"true\"></div>\n</div>",
        "css": "#demo{position:relative;display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif;cursor:none;overflow:hidden}\n.blend-title{margin:0;font-size:64px;line-height:.95;letter-spacing:.02em;color:#e8ecf3;text-align:center}\n.blend-note{margin:12px 0 0;font-size:13px;color:#8b95a8}\n.blend-cursor{position:fixed;top:0;left:0;width:64px;height:64px;border-radius:50%;background:#fff;mix-blend-mode:difference;pointer-events:none;translate:-50% -50%;will-change:transform}\n@media (prefers-reduced-motion:reduce){.blend-cursor{display:none}#demo{cursor:auto}}",
        "js": "const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst ball = document.querySelector('.blend-cursor');\nif (!reduced && ball) {\n  let x = innerWidth / 2, y = innerHeight / 2, cx = x, cy = y;\n  const place = () => { ball.style.transform = `translate3d(${cx}px,${cy}px,0)`; };\n  place();\n  addEventListener('pointermove', (e) => { x = e.clientX; y = e.clientY; });\n  (function frame() {\n    cx += (x - cx) * .2;\n    cy += (y - cy) * .2;\n    place();\n    requestAnimationFrame(frame);\n  })();\n}"
      },
      "source": {
        "name": "MDN — mix-blend-mode",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/mix-blend-mode",
        "domain": "developer.mozilla.org"
      },
      "performance": "Transform — compositor-only; mix-blend-mode создаёт stacking context и пересчитывает композитинг под элементом — держать круг небольшим, не растягивать на весь экран.",
      "a11y": "prefers-reduced-motion:reduce — круг скрыт, возвращён системный курсор; элемент aria-hidden и pointer-events:none, интерактивность не перехватывается.",
      "browser_support": "mix-blend-mode: Chrome 41+, Safari 8+, Firefox 32+ (Baseline с 2020); translate: Chrome 104+, Safari 14.1+, Firefox 72+.",
      "ai_use": "Add a fixed white circle with mix-blend-mode:difference that lerp-follows the pointer at 0.2 per frame; hide it and keep the native cursor under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-button-fill-swipe",
      "name": "Button Fill Swipe",
      "name_ru": "Заливка кнопки по свайпу",
      "category": "hover",
      "tags": [
        "hover",
        "button",
        "pseudo-element",
        "transform",
        "fill"
      ],
      "summary": "Фон кнопки «втекает» слева направо: цветной слой растягивается от левого края, а текст меняет цвет на контрастный.",
      "effect": "Псевдоэлемент ::before с inset:0, transform:scaleX(0) и transform-origin:left; на hover scaleX переходит в 1 за 350ms cubic-bezier(.4,0,.2,1). Текст лежит поверх (z-index) и меняет color за 300ms с задержкой 50ms.",
      "when_to_use": "Вторичные CTA «Узнать больше», кнопки форм, карточки тарифов.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <button class=\"fill-btn\" type=\"button\">Start free trial</button>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.fill-btn{\n  position:relative;overflow:hidden;padding:15px 34px;border:1.5px solid #4f7cff;border-radius:10px;\n  background:transparent;color:#8fb0ff;font:600 15px/1 system-ui,sans-serif;cursor:pointer;\n  transition:color .3s ease .05s;\n}\n.fill-btn::before{\n  content:'';position:absolute;inset:0;background:#4f7cff;\n  transform:scaleX(0);transform-origin:left;\n  transition:transform .35s cubic-bezier(.4,0,.2,1);\n}\n.fill-btn:hover{color:#08101f}\n.fill-btn:hover::before{transform:scaleX(1)}\n.fill-btn:focus-visible{outline:2px solid #8fb0ff;outline-offset:3px}\n@media (prefers-reduced-motion:reduce){.fill-btn::before{transition:none}}",
        "js": ""
      },
      "source": {
        "name": "MDN — transform-origin",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin",
        "domain": "developer.mozilla.org"
      },
      "performance": "Compositor-only: transform на ::before; overflow:hidden обрезает слой по границам кнопки.",
      "a11y": "prefers-reduced-motion:reduce — убирается transition (состояния меняются мгновенно); hover продублирован :focus-visible.",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 3.5+ (transform-origin keywords); inset: Chrome 87+, Safari 14.1+, Firefox 66+.",
      "ai_use": "Give outline buttons a ::before fill layer scaled to 0 from the left edge; on hover/focus scaleX(1) in 350ms cubic-bezier(.4,0,.2,1) while the label color flips; make the state change instant under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-button-text-roll",
      "name": "Button Text Roll",
      "name_ru": "Прокрутка текста в кнопке",
      "category": "hover",
      "tags": [
        "hover",
        "button",
        "text-swap",
        "overflow-clip",
        "micro-interaction"
      ],
      "summary": "В окне фиксированной высоты первая строка уезжает вверх, а снизу поднимается вторая — текст кнопки меняется без сдвига ширины.",
      "effect": "Два span сложены в колонку внутри overflow:hidden высотой 52px; на hover колонка уезжает на -52px за 450ms cubic-bezier(.65,.05,.36,1) — плавный ролл, обратный ход — та же кривая.",
      "when_to_use": "CTA со сменой состояния: «Book a demo → Let's talk», кнопки add-to-cart.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <button class=\"roll-btn\" type=\"button\">\n    <span class=\"roll\"><span>Book a demo</span><span aria-hidden=\"true\">Let's talk →</span></span>\n  </button>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.roll-btn{\n  padding:0 30px;border:0;border-radius:12px;background:#151a26;color:#e8ecf3;\n  font-size:16px;font-weight:600;cursor:pointer;\n  box-shadow:inset 0 0 0 1px #2a3247;\n}\n.roll{display:flex;flex-direction:column;height:52px;overflow:hidden;text-align:left}\n.roll>span{\n  display:flex;align-items:center;height:52px;\n  transition:transform .45s cubic-bezier(.65,.05,.36,1);\n}\n.roll-btn:hover .roll>span,.roll-btn:focus-visible .roll>span{transform:translateY(-52px)}\n@media (prefers-reduced-motion:reduce){.roll>span{transition:none}}",
        "js": ""
      },
      "source": {
        "name": "Motion — Rolling text button",
        "url": "https://motion.dev/ui/components/rolling-text-button",
        "domain": "motion.dev"
      },
      "performance": "Compositor-only translateY; высота окна зафиксирована, ширина кнопки не меняется при подмене текста.",
      "a11y": "Второй span помечен aria-hidden=\"true\", доступное имя кнопки — первый текст; при reduced motion текст меняется мгновенно, hover дублирован focus-visible.",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 4+ (flex + transition + transform).",
      "ai_use": "For CTA buttons stack two labels in a clipped column (fixed row height) and on hover/focus translate the column up by one row (450ms cubic-bezier(.65,.05,.36,1)); keep the second label aria-hidden so the accessible name is stable.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-button-shine-sweep",
      "name": "Button Shine Sweep",
      "name_ru": "Блик по кнопке",
      "category": "hover",
      "tags": [
        "hover",
        "button",
        "gloss",
        "gradient",
        "shine"
      ],
      "summary": "По кнопке при наведении проходит диагональный световой блик — глянцевый отблеск, будто по полированному металлу.",
      "effect": "::after — наклонённая полоса linear-gradient(100deg, transparent, rgba(255,255,255,.5), transparent) шириной 44px; из позиции translateX(-140%) skewX(-18deg) она уезжает в translateX(560%) за 650ms cubic-bezier(.4,0,.2,1).",
      "when_to_use": "Акцентные CTA на градиенте, «премиум»-кнопки тарифов, промо-кнопки.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <button class=\"shine-btn\" type=\"button\">View pricing</button>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.shine-btn{\n  position:relative;overflow:hidden;padding:15px 36px;border:0;border-radius:10px;\n  background:linear-gradient(135deg,#2f6bff,#22d3ee);color:#08101f;\n  font-size:15px;font-weight:700;cursor:pointer;\n}\n.shine-btn::after{\n  content:'';position:absolute;top:-25%;left:0;width:44px;height:150%;\n  background:linear-gradient(100deg,transparent,rgba(255,255,255,.5),transparent);\n  transform:translateX(-140%) skewX(-18deg);\n  transition:transform .65s cubic-bezier(.4,0,.2,1);\n}\n.shine-btn:hover::after,.shine-btn:focus-visible::after{transform:translateX(560%) skewX(-18deg)}\n@media (prefers-reduced-motion:reduce){.shine-btn::after{display:none}.shine-btn:hover{filter:brightness(1.08)}}",
        "js": ""
      },
      "source": {
        "name": "CodePen — CSS Button Shine Effect (digital_playground)",
        "url": "https://codepen.io/digital_playground/pen/RwaVXOL",
        "domain": "codepen.io"
      },
      "performance": "Compositor-only: один transform по ::after; клип по overflow:hidden.",
      "a11y": "prefers-reduced-motion:reduce — блик выключен, обратную связь даёт статичное повышение яркости; hover продублирован focus-visible.",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 3.5+ (gradients и transform на псевдоэлементах).",
      "ai_use": "Add a skewed white gradient streak to accent buttons and sweep it across with translateX over 650ms cubic-bezier(.4,0,.2,1) on hover; remove the streak and use a brightness lift under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-link-underline-slide",
      "name": "Link Underline Slide-In from Left",
      "name_ru": "Подчёркивание выезжает слева",
      "category": "hover",
      "tags": [
        "hover",
        "link",
        "underline",
        "background-size",
        "text"
      ],
      "summary": "Подчёркивание ссылки выезжает слева направо на наведении и работает даже на многострочных ссылках.",
      "effect": "Линия нарисована как background-image:linear-gradient(currentColor,currentColor), прижата к низу (0 100%) и имеет background-size 0% 2px; на hover/focus размер растёт до 100% 2px за 300ms ease.",
      "when_to_use": "Инлайн-ссылки в копирайте, статьи и футер — там, где важно не ломать перенос строк.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <p class=\"copy\">Read the <a class=\"ln-slide\" href=\"#\">migration guide</a> before you ship v2.</p>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif;padding:24px}\n.copy{\n  margin:0;max-width:340px;font-size:19px;line-height:1.55;color:#c6cdda;\n}\n.ln-slide{\n  color:#8fb0ff;text-decoration:none;\n  background-image:linear-gradient(currentColor,currentColor);\n  background-repeat:no-repeat;background-position:0 100%;background-size:0% 2px;\n  transition:background-size .3s ease;\n}\n.ln-slide:hover,.ln-slide:focus-visible{background-size:100% 2px}\n@media (prefers-reduced-motion:reduce){.ln-slide{transition:none;background-size:100% 2px}}",
        "js": ""
      },
      "source": {
        "name": "Danny Guo — Animated Multiline Link Underlines with CSS",
        "url": "https://www.dannyguo.com/blog/animated-multiline-link-underlines-with-css/",
        "domain": "www.dannyguo.com"
      },
      "performance": "Анимация background-size — paint, не compositor, но на маленькой ссылке дёшево; зато корректно работает на переносах строк.",
      "a11y": "Эффект срабатывает и на :focus-visible (клавиатура); при reduced motion линия показана сразу без анимации.",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 3.6+ (multiple backgrounds + background-size transitions).",
      "ai_use": "For inline text links draw the underline as a bottom-aligned linear-gradient background sized 0% 2px; grow to 100% 2px on hover/focus in 300ms ease; show the solid underline with no transition under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": ".ln-slide",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-link-underline-center",
      "name": "Link Underline Center-Out",
      "name_ru": "Подчёркивание из центра",
      "category": "hover",
      "tags": [
        "hover",
        "link",
        "underline",
        "transform",
        "scale"
      ],
      "summary": "Линия под ссылкой раскрывается из центра к краям и так же сворачивается обратно в центр при уходе курсора.",
      "effect": "::after — полоска 2px во всю ширину, transform:scaleX(0) с transform-origin:center; на hover/focus scaleX(1) за 300ms ease, при уходе мыши линия схлопывается обратно в середину.",
      "when_to_use": "Ссылки навигации, футер, ссылки в карточках и списках.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <p class=\"copy\"><a class=\"ln-center\" href=\"#\">API reference</a> is updated every Friday.</p>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif;padding:24px}\n.copy{\n  margin:0;font-size:19px;color:#c6cdda;\n}\n.ln-center{position:relative;color:#8fb0ff;text-decoration:none}\n.ln-center::after{\n  content:'';position:absolute;left:0;bottom:-3px;width:100%;height:2px;\n  background:currentColor;transform:scaleX(0);transform-origin:center;\n  transition:transform .3s ease;\n}\n.ln-center:hover::after,.ln-center:focus-visible::after{transform:scaleX(1)}\n@media (prefers-reduced-motion:reduce){.ln-center::after{transition:none;transform:scaleX(1)}}",
        "js": ""
      },
      "source": {
        "name": "Tobias Ahlin — Animating Link Underlines",
        "url": "https://tobiasahlin.com/blog/css-trick-animating-link-underlines/",
        "domain": "tobiasahlin.com"
      },
      "performance": "Compositor-only: transform на ::after, layout не затрагивается.",
      "a11y": "Работает на :focus-visible; при reduced motion линия видна сразу — состояние не теряется.",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 4+ (pseudo-element + transform transitions).",
      "ai_use": "Give links a 2px ::after underline with scaleX(0) from center; scaleX(1) on hover/focus in 300ms ease; keep it visible without transition under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": ".ln-center",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-card-3d-tilt",
      "name": "Card 3D Tilt",
      "name_ru": "3D-наклон карточки за курсором",
      "category": "hover",
      "tags": [
        "hover",
        "3d",
        "perspective",
        "transform",
        "mousemove"
      ],
      "summary": "Карточка наклоняется в 3D вслед за мышью: край под курсором будто приподнимается, давая лёгкий объём.",
      "effect": "Обёртка даёт perspective:700px; на pointermove считаются нормированные координаты курсора и ставятся rotateX/rotateY до ±12deg с transition 180ms ease-out; на pointerleave — возврат к 0 за 500ms. В демо раз в 2.6s — авто-наклон, чтобы эффект был виден без движения мыши.",
      "when_to_use": "Карточки фич, тарифы, витрина товаров; 1–6 карточек на экран, не больше.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div id=\"demo\">\n  <div class=\"tilt-wrap\">\n    <article class=\"tilt-card\">\n      <h3>Analytics</h3>\n      <p>Realtime events, 14-day retention, no sampling.</p>\n    </article>\n  </div>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.tilt-wrap{perspective:700px}\n.tilt-card{width:230px;padding:22px;border-radius:16px;background:linear-gradient(160deg,#171d2b,#0f131c);border:1px solid #262e40;color:#e8ecf3;transition:transform .18s ease-out;will-change:transform}\n.tilt-card h3{margin:0 0 8px;font-size:18px}\n.tilt-card p{margin:0;font-size:13px;line-height:1.5;color:#9aa5b8}\n@media (prefers-reduced-motion:reduce){.tilt-card{transition:none}}",
        "js": "const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst card = document.querySelector('.tilt-card');\nlet rect, active = false;\nconst tilt = (px, py) => {\n  card.style.transform = `rotateX(${(-py * 12).toFixed(2)}deg) rotateY(${(px * 12).toFixed(2)}deg)`;\n};\nif (!reduced && card) {\n  card.addEventListener('pointerenter', () => { active = true; rect = card.getBoundingClientRect(); });\n  card.addEventListener('pointermove', (e) => {\n    if (!rect) rect = card.getBoundingClientRect();\n    tilt((e.clientX - rect.left) / rect.width - .5, (e.clientY - rect.top) / rect.height - .5);\n  });\n  card.addEventListener('pointerleave', () => {\n    active = false;\n    card.style.transition = 'transform .5s ease';\n    tilt(0, 0);\n    setTimeout(() => { card.style.transition = ''; }, 550);\n  });\n  setInterval(() => {\n    if (active) return;\n    card.style.transition = 'transform .4s ease';\n    tilt(.45, -.3);\n    setTimeout(() => tilt(0, 0), 800);\n  }, 2600);\n}"
      },
      "source": {
        "name": "CSS-Tricks — Animate a Container on Mouse Over Using Perspective and Transform",
        "url": "https://css-tricks.com/animate-a-container-on-mouse-over-using-perspective-and-transform/",
        "domain": "css-tricks.com"
      },
      "performance": "Compositor-only transform; rect кэшируется на pointerenter, поэтому нет layout-thrash на каждый кадр.",
      "a11y": "prefers-reduced-motion:reduce — JS не активируется, карточка остаётся статичной, контент доступен.",
      "browser_support": "Chrome 36+, Safari 9+, Firefox 16+ (CSS perspective и 3D-трансформации).",
      "ai_use": "Wrap card in a perspective:700px container; on pointermove rotateX/rotateY up to 12deg based on cursor offset from center (180ms ease-out), reset on pointerleave in 500ms; do nothing under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-card-lift-glow",
      "name": "Card Lift + Shadow Glow",
      "name_ru": "Подъём карточки со свечением",
      "category": "hover",
      "tags": [
        "hover",
        "card",
        "lift",
        "box-shadow",
        "glow"
      ],
      "summary": "Карточка приподнимается на 6px, а обычная тень превращается в цветное свечение — самый простой способ показать интерактивность.",
      "effect": "transition 300ms cubic-bezier(.2,.7,.2,1) на transform, box-shadow и border-color: translateY(-6px) плюс тень 0 18px 40px rgba(56,132,255,.35) и подсветка рамки rgba(86,156,255,.6).",
      "when_to_use": "Тарифы, карточки цен, фичи и любые кликабельные плитки.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <article class=\"lift-card\">\n    <span class=\"pill\">Most popular</span>\n    <h3>Team plan</h3>\n    <p>$24 / editor / month</p>\n  </article>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.lift-card{\n  width:220px;padding:22px;border-radius:16px;background:#141926;border:1px solid #242d40;\n  box-shadow:0 8px 20px rgba(0,0,0,.4);color:#e8ecf3;\n  transition:transform .3s cubic-bezier(.2,.7,.2,1),box-shadow .3s cubic-bezier(.2,.7,.2,1),border-color .3s;\n}\n.lift-card:hover,.lift-card:focus-within{\n  transform:translateY(-6px);border-color:rgba(86,156,255,.6);\n  box-shadow:0 18px 40px rgba(56,132,255,.35),0 0 0 1px rgba(86,156,255,.35);\n}\n.pill{display:inline-block;margin-bottom:10px;padding:3px 10px;border-radius:999px;background:rgba(79,124,255,.16);color:#8fb0ff;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}\n.lift-card h3{margin:0 0 6px;font-size:18px}\n.lift-card p{margin:0;font-size:14px;color:#9aa5b8}\n@media (prefers-reduced-motion:reduce){.lift-card{transition:box-shadow .3s,border-color .3s}.lift-card:hover{transform:none}}",
        "js": ""
      },
      "source": {
        "name": "MDN — box-shadow",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow",
        "domain": "developer.mozilla.org"
      },
      "performance": "transform — compositor; box-shadow — paint-анимация, для одной карточки дёшево, не вешать на десятки элементов сразу.",
      "a11y": "prefers-reduced-motion:reduce — подъём отключён, остаются свечение и подсветка рамки; hover продублирован :focus-within.",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 3.5+ (box-shadow + transitions).",
      "ai_use": "On card hover/focus-within translateY(-6px) and swap the shadow for a colored glow (0 18px 40px rgba(56,132,255,.35)) over 300ms cubic-bezier(.2,.7,.2,1); under reduced motion keep only the shadow/border change.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-gradient-border",
      "name": "Gradient Border on Hover",
      "name_ru": "Градиентная рамка по ховеру",
      "category": "hover",
      "tags": [
        "hover",
        "card",
        "conic-gradient",
        "mask",
        "border"
      ],
      "summary": "Вокруг карточки на наведении проявляется коническая градиентная рамка — тонкое цветное кольцо по периметру.",
      "effect": "::before с padding:2px рисует кольцо: фон — conic-gradient(from 140deg, #4f7cff, #22d3ee, #a855f7, #4f7cff), а две маски (content-box и border-box) с mask-composite:exclude оставляют видимой только кромку; opacity 0→1 за 350ms ease.",
      "when_to_use": "Выделенный тариф, «pro»-карточка, интерфейсы с неоновой эстетикой.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div id=\"demo\">\n  <article class=\"gb-card\">\n    <h3>Pro workspace</h3>\n    <p>Unlimited projects, guests and audit log.</p>\n  </article>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.gb-card{position:relative;width:240px;padding:24px;border-radius:16px;background:#12151d;border:1px solid #222a3b;color:#e8ecf3}\n.gb-card h3{margin:0 0 8px;font-size:18px}\n.gb-card p{margin:0;font-size:13px;line-height:1.5;color:#9aa5b8}\n.gb-card::before{\n  content:'';position:absolute;inset:0;padding:2px;border-radius:inherit;\n  background:conic-gradient(from 140deg,#4f7cff,#22d3ee,#a855f7,#4f7cff);\n  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);\n  -webkit-mask-composite:xor;\n  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);\n  mask-composite:exclude;\n  opacity:0;transition:opacity .35s ease;pointer-events:none;\n}\n.gb-card:hover::before,.gb-card:focus-within::before{opacity:1}\n@media (prefers-reduced-motion:reduce){.gb-card::before{transition:none}}",
        "js": ""
      },
      "source": {
        "name": "web.dev — Use conic gradients to create a cool border",
        "url": "https://web.dev/articles/conic-gradient-border",
        "domain": "web.dev"
      },
      "performance": "Анимируется только opacity — compositor-friendly; conic-gradient рендерится один раз, маски не пересчитываются.",
      "a11y": "prefers-reduced-motion:reduce — рамка появляется мгновенно; состояние работает и на :focus-within.",
      "browser_support": "Chrome 120+ (mask-composite:exclude; до этого -webkit-mask-composite:xor), Safari 15.4+, Firefox 53+.",
      "ai_use": "Add a gradient ring to feature cards: ::before with padding:2px, conic-gradient background and dual-mask exclude-composite; fade opacity in 350ms on hover/focus-within; instant appearance under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-image-zoom",
      "name": "Image Zoom Inside Card",
      "name_ru": "Зум изображения в карточке",
      "category": "hover",
      "tags": [
        "hover",
        "image",
        "zoom",
        "scale",
        "overflow-clip"
      ],
      "summary": "Внутри карточки изображение плавно приближается, не выходя за скруглённые края кадра.",
      "effect": "Кадр с overflow:hidden; слой изображения получает transform:scale(1.12) на hover за 500ms cubic-bezier(.22,.68,.28,1); подпись под кадром не масштабируется, поэтому текст остаётся резким.",
      "when_to_use": "Карточки товаров, статей, туров, кейсов — стандартный приём для сеток.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <figure class=\"zoomer\">\n    <div class=\"photo\" role=\"img\" aria-label=\"Sunrise over the coast\"></div>\n    <figcaption>Sunrise route · 12 km</figcaption>\n  </figure>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.zoomer{width:250px;margin:0;border-radius:14px;overflow:hidden;background:#141926;border:1px solid #242d40}\n.photo{\n  height:160px;\n  background:radial-gradient(circle at 72% 26%,#ffd08a 0 9%,transparent 10%),\n    linear-gradient(#17293d 0 52%,#1d4356 52% 66%,#0c1a26 66% 100%);\n  transition:transform .5s cubic-bezier(.22,.68,.28,1);\n}\n.zoomer:hover .photo{transform:scale(1.12)}\n.zoomer figcaption{padding:10px 14px;font-size:13px;color:#9aa5b8}\n@media (prefers-reduced-motion:reduce){.photo{transition:filter .2s ease}.zoomer:hover .photo{transform:none;filter:brightness(1.08)}}",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks — Zooming Background Images",
        "url": "https://css-tricks.com/zooming-background-images/",
        "domain": "css-tricks.com"
      },
      "performance": "Compositor-only scale; кадр зафиксирован и обрезает зум, соседний layout не двигается.",
      "a11y": "Изображение описано через role=img + aria-label; при reduced motion вместо зума — лёгкое изменение яркости.",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 3.5+ (transform scale).",
      "ai_use": "Place media inside an overflow:hidden rounded frame; scale the inner visual to 1.12 on card hover in 500ms cubic-bezier(.22,.68,.28,1); keep captions outside the scaling layer; swap zoom for brightness under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-reveal-content",
      "name": "Reveal-on-Hover Content",
      "name_ru": "Раскрытие контента по ховеру",
      "category": "hover",
      "tags": [
        "hover",
        "card",
        "clip-path",
        "reveal",
        "inset"
      ],
      "summary": "Скрытый слой с деталями выезжает снизу поверх карточки через clip-path и не сдвигает соседние элементы.",
      "effect": "Слой лежит inset:0 и обрезан clip-path:inset(100% 0 0 0); на hover clip-path переходит в inset(0 0 0 0) за 400ms cubic-bezier(.2,.8,.2,1) — панель с деталями раскрывается снизу вверх.",
      "when_to_use": "Карточки кейсов, фичи «что внутри», плитки с подробностями по наведению.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <article class=\"reveal-card\">\n    <div class=\"reveal-base\">\n      <h3>Self-hosted</h3>\n      <p>Deploy inside your own VPC.</p>\n    </div>\n    <div class=\"reveal-layer\" aria-hidden=\"true\">\n      <p>No vendor lock-in. SOC 2 ready. Bring your own cloud.</p>\n    </div>\n  </article>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.reveal-card{position:relative;width:250px;height:150px;border-radius:16px;overflow:hidden;background:#141926;border:1px solid #242d40;color:#e8ecf3}\n.reveal-base{padding:20px}\n.reveal-base h3{margin:0 0 6px;font-size:18px}\n.reveal-base p{margin:0;font-size:13px;color:#9aa5b8}\n.reveal-layer{position:absolute;inset:0;display:grid;place-items:center;padding:22px;background:linear-gradient(135deg,#2f6bff,#22d3ee);color:#08101f;font-size:14px;font-weight:600;line-height:1.45;clip-path:inset(100% 0 0 0);transition:clip-path .4s cubic-bezier(.2,.8,.2,1)}\n.reveal-layer p{margin:0}\n.reveal-card:hover .reveal-layer,.reveal-card:focus-within .reveal-layer{clip-path:inset(0 0 0 0)}\n@media (prefers-reduced-motion:reduce){.reveal-layer{transition:none}}",
        "js": ""
      },
      "source": {
        "name": "MDN — clip-path",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path",
        "domain": "developer.mozilla.org"
      },
      "performance": "clip-path меняет только видимую область — коробка фиксирована, reflow соседей не происходит; на больших медиа следить за перерисовкой.",
      "a11y": "Слой с деталями декоративный (aria-hidden), ключевая информация продублирована в базовом слое; при reduced motion панель раскрывается мгновенно.",
      "browser_support": "Chrome 55+, Safari 9.1+ (с префиксом) / 13.1+ без префикса, Firefox 54+.",
      "ai_use": "Overlay a detail panel on cards hidden with clip-path inset(100% 0 0 0); reveal to inset(0 0 0 0) on hover/focus-within in 400ms cubic-bezier(.2,.8,.2,1); instant reveal under reduced motion; keep key info outside the decorative layer.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-ripple-click",
      "name": "Ripple on Click (Material)",
      "name_ru": "Волна от клика (Material)",
      "category": "hover",
      "tags": [
        "hover",
        "click",
        "ripple",
        "material",
        "feedback"
      ],
      "summary": "Из точки клика по кнопке расходится круглая волна — фирменная обратная связь Material Design.",
      "effect": "На pointerdown из координат курсора создаётся span диаметром с максимум стороны кнопки; keyframes ripple: transform scale(0)→scale(2.5) и opacity→0 за 650ms ease-out; предыдущие волны удаляются. В демо волна также запускается на pointerenter и таймером раз в 1.8s, чтобы эффект было видно в записи без клика.",
      "when_to_use": "Кнопки форм, «добавить в корзину», любые кликабельные CTA в материал-стиле.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <button class=\"ripple-btn\" type=\"button\">Add to workspace</button>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.ripple-btn{position:relative;overflow:hidden;padding:15px 32px;border:0;border-radius:10px;background:#2f6bff;color:#fff;font-size:15px;font-weight:600;cursor:pointer;transition:background .3s ease}\n.ripple-btn:hover{background:#3d78ff}\n.ripple{position:absolute;border-radius:50%;background:rgba(255,255,255,.5);transform:scale(0);pointer-events:none;animation:ripple .65s ease-out forwards}\n@keyframes ripple{to{transform:scale(2.5);opacity:0}}\n@media (prefers-reduced-motion:reduce){.ripple{animation:none;opacity:0}}",
        "js": "const btn = document.querySelector('.ripple-btn');\nconst spawn = (x, y) => {\n  const rect = btn.getBoundingClientRect();\n  const d = Math.max(rect.width, rect.height);\n  const s = document.createElement('span');\n  s.className = 'ripple';\n  s.style.width = s.style.height = `${d}px`;\n  s.style.left = `${(x ?? rect.left + rect.width / 2) - rect.left - d / 2}px`;\n  s.style.top = `${(y ?? rect.top + rect.height / 2) - rect.top - d / 2}px`;\n  btn.querySelectorAll('.ripple').forEach((old) => old.remove());\n  btn.appendChild(s);\n};\nif (!matchMedia('(prefers-reduced-motion: reduce)').matches) {\n  btn.addEventListener('pointerdown', (e) => spawn(e.clientX, e.clientY));\n  btn.addEventListener('pointerenter', () => spawn());\n  setInterval(() => spawn(), 1800);\n}"
      },
      "source": {
        "name": "CSS-Tricks — How to Recreate the Ripple Effect of Material Design Buttons",
        "url": "https://css-tricks.com/how-to-recreate-the-ripple-effect-of-material-design-buttons/",
        "domain": "css-tricks.com"
      },
      "performance": "Анимируются только transform и opacity одноразового span; старые узлы удаляются, чтобы не копить DOM.",
      "a11y": "prefers-reduced-motion:reduce — волны не создаются вовсе, остаётся hover-затемнение; клик не блокируется, кнопка полностью функциональна.",
      "browser_support": "Chrome 49+, Safari 10+, Firefox 45+ (getBoundingClientRect, classList, CSS animations).",
      "ai_use": "On button pointerdown spawn a circle at the click point sized to the button's largest side; animate scale(0)→2.5 with fade in 650ms ease-out; remove previous ripples; disable under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "hover-tooltip-pop",
      "name": "Tooltip Pop",
      "name_ru": "Тултип-поповер над элементом",
      "category": "hover",
      "tags": [
        "hover",
        "tooltip",
        "popover",
        "aria",
        "micro-interaction"
      ],
      "summary": "Над элементом всплывает подсказка с лёгким пружинным масштабом; закрывается при уходе курсора и по Escape.",
      "effect": "Слой role=tooltip спрятан (opacity:0, translate(-50%,6px) scale(.96)); на hover/focus-visible — opacity:1, translate(-50%,0) scale(1) за 180ms cubic-bezier(.34,1.56,.64,1); стрелка — border-треугольник, position:absolute над триггером.",
      "when_to_use": "Иконки-кнопки без подписи, термины, подсказки в тулбарах.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div id=\"demo\">\n  <span class=\"tip-wrap\">\n    <button class=\"tip-trigger\" type=\"button\" aria-describedby=\"tip-copy\">Copy invite link</button>\n    <span class=\"tip\" role=\"tooltip\" id=\"tip-copy\">Invite link copied</span>\n  </span>\n</div>",
        "css": "#demo{display:grid;place-items:center;min-height:320px;background:#0b0d12;font-family:system-ui,sans-serif}\n.tip-wrap{position:relative;display:inline-block}\n.tip-trigger{padding:12px 22px;border-radius:10px;border:1px solid #2c3550;background:#151a26;color:#e8ecf3;font-size:14px;cursor:pointer}\n.tip{position:absolute;bottom:calc(100% + 10px);left:50%;padding:8px 12px;border-radius:8px;background:#1d2436;border:1px solid #303a55;color:#e8ecf3;font-size:13px;white-space:nowrap;opacity:0;transform:translate(-50%,6px) scale(.96);pointer-events:none;transition:opacity .18s ease,transform .18s cubic-bezier(.34,1.56,.64,1)}\n.tip::after{content:'';position:absolute;top:100%;left:50%;margin-left:-5px;border:5px solid transparent;border-top-color:#1d2436}\n.tip-trigger:hover+.tip,.tip-trigger:focus-visible+.tip{opacity:1;transform:translate(-50%,0) scale(1)}\n@media (prefers-reduced-motion:reduce){.tip{transition:opacity .15s ease}}",
        "js": "document.addEventListener('keydown', (e) => {\n  if (e.key === 'Escape') {\n    document.querySelectorAll('.tip').forEach((tip) => { tip.style.opacity = 0; });\n  }\n});"
      },
      "source": {
        "name": "W3C WAI — ARIA APG Tooltip Pattern",
        "url": "https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/",
        "domain": "www.w3.org"
      },
      "performance": "opacity/transform, элемент инертен (pointer-events:none), позиционируется absolute — layout не сдвигается.",
      "a11y": "role=tooltip + aria-describedby на триггере; показ на focus-visible, закрытие по Escape; при reduced motion тултип появляется без пружины (только opacity).",
      "browser_support": "Chrome 4+, Safari 3.1+, Firefox 3.5+ (transitions); role=tooltip поддерживается современными скринридерами.",
      "ai_use": "Attach a role=tooltip bubble above the trigger referenced with aria-describedby; pop it in on hover/focus-visible with 180ms spring cubic-bezier(.34,1.56,.64,1); close on Escape; simplify to a plain fade under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": "#demo",
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-04-hover.json"
    },
    {
      "id": "nav-fullscreen-menu-clip-path",
      "name": "Fullscreen Menu Clip-Path Reveal",
      "name_ru": "Полноэкранное меню с clip-path-раскрытием",
      "category": "navigation",
      "tags": [
        "menu",
        "clip-path",
        "overlay",
        "fullscreen",
        "circle-reveal"
      ],
      "summary": "Кнопка «Menu» раскрывает полноэкранный оверлей круговым clip-path из угла кнопки, после чего пункты меню всплывают по очереди.",
      "effect": "Оверлей разворачивается clip-path от circle(0% at calc(100% - 56px) 34px) до circle(145%) за 700мс cubic-bezier(.76,0,.24,1). Ссылки появляются снизу на 18px, opacity 0→1, задержка 250мс + индекс * 90мс.",
      "when_to_use": "Мобильная и планшетная навигация на лендингах, где нужен вау-эффект: вместо бургер-дропдауна полноэкранное меню из кнопки в шапке.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo nav1\">\n  <header class=\"nav1-bar\">\n    <span class=\"nav1-logo\">Nordwave</span>\n    <button class=\"nav1-toggle\" id=\"nav1-open\" aria-expanded=\"false\" aria-controls=\"nav1-menu\">Menu</button>\n  </header>\n  <div class=\"nav1-menu\" id=\"nav1-menu\">\n    <nav>\n      <a href=\"#work\" style=\"--i:0\">Work</a>\n      <a href=\"#studio\" style=\"--i:1\">Studio</a>\n      <a href=\"#blog\" style=\"--i:2\">Blog</a>\n      <a href=\"#contact\" style=\"--i:3\">Contact</a>\n    </nav>\n  </div>\n</div>",
        "css": ".nav1{position:relative;height:320px;overflow:hidden;background:#0b0d12;color:#e8eaf2;font:600 14px/1.4 system-ui,sans-serif}\n.nav1-bar{display:flex;justify-content:space-between;align-items:center;padding:14px 16px}\n.nav1-toggle{background:#151927;color:#e8eaf2;border:1px solid #2a2f3d;border-radius:999px;padding:8px 14px;cursor:pointer}\n.nav1-menu{position:absolute;inset:0;display:grid;place-items:center;background:#111527;visibility:hidden;clip-path:circle(0 at calc(100% - 56px) 34px);transition:clip-path .7s cubic-bezier(.76,0,.24,1),visibility 0s linear .7s}\n.nav1-menu.is-open{visibility:visible;clip-path:circle(145% at calc(100% - 56px) 34px);transition:clip-path .7s cubic-bezier(.76,0,.24,1),visibility 0s}\n.nav1-menu a{display:block;padding:8px;color:#e8eaf2;text-decoration:none;font-size:26px;font-weight:700;opacity:0;transform:translateY(18px);transition:opacity .4s ease,transform .45s cubic-bezier(.2,.7,.2,1)}\n.nav1-menu.is-open a{opacity:1;transform:none;transition-delay:calc(.25s + var(--i)*.09s)}\n@media (prefers-reduced-motion:reduce){.nav1-menu,.nav1-menu a{transition:none}}",
        "js": "const menu=document.getElementById('nav1-menu'),btn=document.getElementById('nav1-open');\nfunction set(v){menu.classList.toggle('is-open',v);btn.setAttribute('aria-expanded',v)}\nbtn.addEventListener('click',()=>set(!menu.classList.contains('is-open')));\nmenu.querySelectorAll('a').forEach(a=>a.addEventListener('click',()=>set(false)));\nsetTimeout(()=>set(true),1200);"
      },
      "source": {
        "name": "CSS-Tricks — Animating with Clip-Path",
        "url": "https://css-tricks.com/animating-with-clip-path",
        "domain": "css-tricks.com"
      },
      "performance": "clip-path и opacity — только композитор, без reflow. Оверлей держать вне документа (visibility:hidden) в закрытом состоянии.",
      "a11y": "prefers-reduced-motion: transition:none — меню появляется мгновенно. aria-expanded синхронизирован, ссылки закрывают оверлей по клику.",
      "browser_support": "Chrome 55+, Safari 13.1+, Firefox 54+ (анимация clip-path — те же версии)",
      "ai_use": "Use for a fullscreen nav: overlay panel expands via clip-path circle(0 -> 145%) from the button corner in 700ms cubic-bezier(.76,0,.24,1), then stagger links 90ms with a 250ms base delay; toggle on button click.",
      "preview": {
        "scroll": false,
        "mouse": "#nav1-open",
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-menu-items-stagger",
      "name": "Staggered Menu Items Slide-In",
      "name_ru": "Поочерёдный вход пунктов меню",
      "category": "navigation",
      "tags": [
        "menu",
        "stagger",
        "dropdown",
        "transition-delay",
        "slide"
      ],
      "summary": "Панель меню открывается, а её пункты въезжают слева по одному с инкрементальной задержкой через transition-delay: calc(var(--i) * 70ms).",
      "effect": "Каждый пункт: opacity 0→1 и translateX(-14px → 0) за 400мс cubic-bezier(.2,.7,.2,1); задержка = индекс * 70мс. Закрытие происходит одновременно и быстро.",
      "when_to_use": "Выпадающие меню в шапке лендинга, мега-меню продуктов, любые списки пунктов, которые должны читаться последовательно.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo nav2\">\n  <header class=\"nav2-bar\">\n    <span class=\"nav2-logo\">Pulse</span>\n    <button id=\"nav2-toggle\" class=\"nav2-toggle\" aria-expanded=\"false\">Products</button>\n  </header>\n  <ul class=\"nav2-panel\" id=\"nav2-panel\">\n    <li style=\"--i:0\"><a href=\"#\">Analytics</a></li>\n    <li style=\"--i:1\"><a href=\"#\">Automation</a></li>\n    <li style=\"--i:2\"><a href=\"#\">Integrations</a></li>\n    <li style=\"--i:3\"><a href=\"#\">Changelog</a></li>\n  </ul>\n</div>",
        "css": ".nav2{position:relative;height:320px;overflow:hidden;background:#0b0d12;color:#e8eaf2;font:500 14px/1.4 system-ui,sans-serif}\n.nav2-bar{display:flex;justify-content:space-between;align-items:center;padding:14px 16px}\n.nav2-toggle{background:#1b2233;color:#e8eaf2;border:0;border-radius:10px;padding:9px 14px;cursor:pointer;font:inherit}\n.nav2-panel{position:absolute;left:16px;right:16px;margin:0;padding:8px;list-style:none;background:#141a29;border:1px solid #232c42;border-radius:14px;visibility:hidden}\n.nav2-panel li{opacity:0;transform:translateX(-14px);transition:opacity .35s ease,transform .4s cubic-bezier(.2,.7,.2,1)}\n.nav2-panel.is-open{visibility:visible}\n.nav2-panel.is-open li{opacity:1;transform:none;transition-delay:calc(var(--i)*.07s)}\n.nav2-panel a{display:block;padding:11px 12px;border-radius:9px;color:#cfd6ea;text-decoration:none}\n@media (prefers-reduced-motion:reduce){.nav2-panel li{transition:none}}",
        "js": "const panel=document.getElementById('nav2-panel'),btn=document.getElementById('nav2-toggle');\nfunction flip(v){panel.classList.toggle('is-open',v);btn.setAttribute('aria-expanded',v)}\nbtn.addEventListener('click',()=>flip(!panel.classList.contains('is-open')));\nsetTimeout(()=>flip(true),1200);"
      },
      "source": {
        "name": "CSS-Tricks — Staggering Animations",
        "url": "https://css-tricks.com/staggering-animations",
        "domain": "css-tricks.com"
      },
      "performance": "Композитор-only (opacity + transform). Нет замеров layout; задержки чистые, без таймеров.",
      "a11y": "prefers-reduced-motion: transition:none — пункты видны сразу. aria-expanded обновляется, панель управляется кнопкой.",
      "browser_support": "Все современные браузеры (transition-delay и CSS-переменные в calc)",
      "ai_use": "Use for dropdown/submenu reveals: fade items in with translateX(-14px -> 0) over 400ms cubic-bezier(.2,.7,.2,1) and set transition-delay: calc(var(--i) * 70ms) per item.",
      "preview": {
        "scroll": false,
        "mouse": "#nav2-toggle",
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-modal-scale-pop",
      "name": "Modal Scale From Trigger",
      "name_ru": "Модальное окно, растущее из триггера",
      "category": "navigation",
      "tags": [
        "modal",
        "dialog",
        "scale",
        "backdrop",
        "show-modal"
      ],
      "summary": "Нативный <dialog> открывается через showModal() и появляется масштабированием с лёгким перелётом, фон затемняется через ::backdrop.",
      "effect": "dialog[open]: scale(.86) translateY(10px) → scale(1), opacity 0→1 за 300мс cubic-bezier(.2,1.25,.4,1) — лёгкий overshoot. ::backdrop fade 300мс.",
      "when_to_use": "Подтверждения удаления, формы подписки, быстрые настройки — всё, что требует фокуса без ухода со страницы.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo nav3\">\n  <button id=\"nav3-open\" class=\"nav3-btn\">Delete project</button>\n  <dialog id=\"nav3-modal\" class=\"nav3-modal\">\n    <h3>Delete project?</h3>\n    <p>This removes “Aurora” and its 12 deployments. This cannot be undone.</p>\n    <div class=\"nav3-actions\">\n      <button class=\"nav3-ghost\" data-close>Cancel</button>\n      <button data-close>Delete</button>\n    </div>\n  </dialog>\n</div>",
        "css": ".nav3{height:320px;display:grid;place-items:center;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav3-btn{background:#4f7cff;border:0;color:#fff;padding:10px 16px;border-radius:10px;cursor:pointer;font:inherit}\n.nav3-modal{width:min(340px,86vw);margin:auto;padding:20px;background:#141a29;color:#e8eaf2;border:1px solid #2a3554;border-radius:16px}\n.nav3-modal h3{margin:0 0 8px;font-size:18px}\n.nav3-modal p{margin:0 0 16px;color:#9aa6c4}\n.nav3-modal[open]{animation:nav3-pop .3s cubic-bezier(.2,1.25,.4,1) both}\n.nav3-modal::backdrop{background:rgba(5,8,16,.65);animation:nav3-fade .3s ease both}\n.nav3-actions{display:flex;gap:8px;justify-content:flex-end}\n.nav3-actions button{background:#4f7cff;border:0;color:#fff;padding:8px 14px;border-radius:9px;cursor:pointer;font:inherit}\n.nav3-actions .nav3-ghost{background:#232c42}\n@keyframes nav3-pop{from{opacity:0;transform:scale(.86) translateY(10px)}}\n@keyframes nav3-fade{from{opacity:0}}\n@media (prefers-reduced-motion:reduce){.nav3-modal[open],.nav3-modal::backdrop{animation-duration:.01s}}",
        "js": "const modal=document.getElementById('nav3-modal');\ndocument.getElementById('nav3-open').addEventListener('click',()=>modal.showModal());\nmodal.querySelectorAll('[data-close]').forEach(b=>b.addEventListener('click',()=>modal.close()));\nsetTimeout(()=>{if(!modal.open)modal.showModal()},1200);"
      },
      "source": {
        "name": "web.dev — Building a dialog component",
        "url": "https://web.dev/articles/building/a-dialog-component",
        "domain": "web.dev"
      },
      "performance": "Анимация transform/opacity поверх top layer — композитор-only. showModal() берёт фокус-трап и Esc бесплатно.",
      "a11y": "Нативный <dialog> с focus trap и Esc; prefers-reduced-motion сокращает анимацию до 0.01s, но окно остаётся видимым.",
      "browser_support": "dialog: Chrome 37+, Safari 15.4+, Firefox 98+; scale-анимация — все современные",
      "ai_use": "Open modals with dialog.showModal() and animate [open] scale(.86 -> 1) + fade in 300ms cubic-bezier(.2,1.25,.4,1); fade ::backdrop 300ms.",
      "preview": {
        "scroll": false,
        "mouse": "#nav3-open",
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-bottom-sheet-slide-up",
      "name": "Bottom Sheet Slide-Up",
      "name_ru": "Bottom sheet: выезд снизу",
      "category": "navigation",
      "tags": [
        "bottom-sheet",
        "dialog",
        "slide-up",
        "backdrop",
        "mobile"
      ],
      "summary": "Модальное окно, прижатое к нижнему краю экрана, выезжает снизу на 100% своей высоты с плавным торможением; фон притемняется.",
      "effect": "dialog[open]: translateY(100%) → 0, opacity .4→1 за 380мс cubic-bezier(.16,1,.3,1) (ease-out expo); ::backdrop fade 300мс.",
      "when_to_use": "Мобильные действия: «поделиться», фильтры, выбор адреса, быстрые формы — паттерн нативного bottom sheet.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo nav4\">\n  <button id=\"nav4-open\" class=\"nav4-btn\">Share page</button>\n  <dialog id=\"nav4-sheet\" class=\"nav4-sheet\">\n    <div class=\"nav4-handle\" aria-hidden=\"true\"></div>\n    <h3>Share this page</h3>\n    <p>Send a link to your team, including read access.</p>\n    <div class=\"nav4-row\">\n      <button class=\"nav4-close\" data-close>Close</button>\n      <button>Copy link</button>\n    </div>\n  </dialog>\n</div>",
        "css": ".nav4{height:320px;display:grid;place-items:center;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav4-btn{background:#4f7cff;border:0;color:#fff;padding:10px 16px;border-radius:10px;cursor:pointer;font:inherit}\n.nav4-sheet{position:fixed;inset:auto 0 0 0;width:min(420px,100%);margin:0 auto;padding:8px 18px 20px;background:#141a29;color:#e8eaf2;border:0;border-radius:18px 18px 0 0}\n.nav4-sheet h3{margin:0 0 6px;font-size:17px}\n.nav4-sheet p{margin:0 0 16px;color:#9aa6c4}\n.nav4-sheet[open]{animation:nav4-up .38s cubic-bezier(.16,1,.3,1) both}\n.nav4-sheet::backdrop{background:rgba(5,8,16,.6);animation:nav4-fade .3s ease both}\n.nav4-handle{width:40px;height:4px;margin:4px auto 12px;border-radius:99px;background:#374260}\n.nav4-row{display:flex;gap:8px;justify-content:flex-end}\n.nav4-row button{background:#4f7cff;border:0;color:#fff;padding:9px 14px;border-radius:9px;cursor:pointer;font:inherit}\n.nav4-row .nav4-close{background:#232c42}\n@keyframes nav4-up{from{opacity:.4;transform:translateY(100%)}}\n@keyframes nav4-fade{from{opacity:0}}\n@media (prefers-reduced-motion:reduce){.nav4-sheet[open],.nav4-sheet::backdrop{animation-duration:.01s}}",
        "js": "const sheet=document.getElementById('nav4-sheet');\ndocument.getElementById('nav4-open').addEventListener('click',()=>sheet.showModal());\nsheet.querySelectorAll('[data-close]').forEach(b=>b.addEventListener('click',()=>sheet.close()));\nsetTimeout(()=>{if(!sheet.open)sheet.showModal()},1200);"
      },
      "source": {
        "name": "web.dev — <dialog> and popover: Baseline layered UI patterns",
        "url": "https://web.dev/articles/baseline-in-action-dialog-popover",
        "domain": "web.dev"
      },
      "performance": "transform/opacity по композитору; dialog в top layer не влияет на layout страницы.",
      "a11y": "Нативный <dialog>: Esc, focus trap, aria-modal. prefers-reduced-motion делает появление мгновенным; состояния — чистая функция от open.",
      "browser_support": "dialog: Chrome 37+, Safari 15.4+, Firefox 98+; position:fixed inset — все современные",
      "ai_use": "Use for mobile actions: dialog pinned bottom, animate translateY(100% -> 0) in 380ms cubic-bezier(.16,1,.3,1) with a fading ::backdrop; add drag-to-dismiss on touch.",
      "preview": {
        "scroll": false,
        "mouse": "#nav4-open",
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-drawer-slide-in",
      "name": "Side Drawer Slide-In",
      "name_ru": "Боковая панель (drawer) сбоку",
      "category": "navigation",
      "tags": [
        "drawer",
        "off-canvas",
        "slide",
        "scrim",
        "filters"
      ],
      "summary": "Панель фильтров выезжает справа на 102% ширины за 420мс, одновременно проявляется затемняющий scrim; закрытие — по клику на scrim.",
      "effect": "drawer: translateX(102%) → 0 за 420мс cubic-bezier(.16,1,.3,1) с тенью -18px; scrim fade 300мс opacity 0→1.",
      "when_to_use": "Фильтры каталога, корзина, настройки аккаунта, мобильное меню — контент, который не должен вытеснять основную страницу.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo nav5\">\n  <header class=\"nav5-bar\">\n    <span class=\"nav5-logo\">Atlas</span>\n    <button id=\"nav5-open\" class=\"nav5-open\" aria-expanded=\"false\" aria-controls=\"nav5-drawer\">Filters</button>\n  </header>\n  <div class=\"nav5-scrim\" id=\"nav5-scrim\"></div>\n  <aside class=\"nav5-drawer\" id=\"nav5-drawer\" aria-label=\"Filters\">\n    <h3>Filters</h3>\n    <label><input type=\"checkbox\" checked> In stock</label>\n    <label><input type=\"checkbox\"> On sale</label>\n    <label><input type=\"checkbox\"> Free shipping</label>\n    <button class=\"nav5-apply\" id=\"nav5-close\">Apply</button>\n  </aside>\n</div>",
        "css": ".nav5{position:relative;height:320px;overflow:hidden;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav5-bar{display:flex;justify-content:space-between;align-items:center;padding:14px 16px}\n.nav5-open{background:#1b2233;color:#e8eaf2;border:0;padding:9px 14px;border-radius:10px;cursor:pointer;font:inherit}\n.nav5-scrim{position:absolute;inset:0;background:rgba(4,7,14,.6);opacity:0;pointer-events:none;transition:opacity .3s ease}\n.nav5-scrim.is-open{opacity:1;pointer-events:auto}\n.nav5-drawer{position:absolute;top:0;right:0;bottom:0;width:240px;padding:18px;background:#141a29;box-shadow:-18px 0 40px rgba(0,0,0,.4);transform:translateX(102%);transition:transform .42s cubic-bezier(.16,1,.3,1)}\n.nav5-drawer.is-open{transform:none}\n.nav5-drawer h3{margin:0 0 12px}\n.nav5-drawer label{display:block;margin:10px 0;color:#cfd6ea}\n.nav5-apply{width:100%;margin-top:14px;padding:10px;border:0;border-radius:10px;background:#4f7cff;color:#fff;cursor:pointer;font:inherit}\n@media (prefers-reduced-motion:reduce){.nav5-drawer,.nav5-scrim{transition:none}}",
        "js": "const d=document.getElementById('nav5-drawer'),s=document.getElementById('nav5-scrim'),o=document.getElementById('nav5-open');\nfunction flip(v){d.classList.toggle('is-open',v);s.classList.toggle('is-open',v);o.setAttribute('aria-expanded',v)}\no.addEventListener('click',()=>flip(!d.classList.contains('is-open')));\ns.addEventListener('click',()=>flip(false));\ndocument.getElementById('nav5-close').addEventListener('click',()=>flip(false));\nsetTimeout(()=>flip(true),1200);"
      },
      "source": {
        "name": "web.dev — Building a sidenav component",
        "url": "https://web.dev/articles/building/a-sidenav-component",
        "domain": "web.dev"
      },
      "performance": "transform + opacity — композитор-only. Scrim c pointer-events:none в закрытом состоянии не блокирует клики.",
      "a11y": "prefers-reduced-motion: transition:none; aria-expanded на кнопке, aria-label на панели, закрытие по scrim и кнопке Apply.",
      "browser_support": "Все современные браузеры (transform, transitions)",
      "ai_use": "Use for off-canvas filters/nav: aside translateX(102% -> 0) over 420ms cubic-bezier(.16,1,.3,1) plus a scrim opacity fade; close on scrim click and Esc.",
      "preview": {
        "scroll": false,
        "mouse": "#nav5-open",
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-accordion-grid-expand",
      "name": "Accordion Grid-Row Expand",
      "name_ru": "Плавное раскрытие аккордеона",
      "category": "navigation",
      "tags": [
        "accordion",
        "grid-template-rows",
        "expand",
        "disclosure",
        "faq"
      ],
      "summary": "Аккордеон с анимацией высоты через grid-template-rows 0fr → 1fr — без max-height и без JS-замеров высоты.",
      "effect": "Контейнер панели display:grid, grid-template-rows 0fr → 1fr за 350мс cubic-bezier(.2,.7,.2,1); внутренняя обёртка overflow:hidden; плюсик поворачивается на 45°.",
      "when_to_use": "FAQ-секции лендинга, тарифные детали, длинные списки фич, которые можно сворачивать.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo nav6\">\n  <div class=\"nav6-item\">\n    <button class=\"nav6-head\" aria-expanded=\"false\">What is included?<span aria-hidden=\"true\">+</span></button>\n    <div class=\"nav6-panel\"><div class=\"nav6-clip\"><div class=\"nav6-body\">Design tokens, 40 sections and a Figma library — all in one file.</div></div></div>\n  </div>\n  <div class=\"nav6-item\">\n    <button class=\"nav6-head\" aria-expanded=\"false\">Can I self-host?<span aria-hidden=\"true\">+</span></button>\n    <div class=\"nav6-panel\"><div class=\"nav6-clip\"><div class=\"nav6-body\">Yes. One command deploys the static build to any CDN or your own server.</div></div></div>\n  </div>\n</div>",
        "css": ".nav6{padding:16px;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav6-item{margin-bottom:10px;background:#121726;border:1px solid #232c42;border-radius:12px}\n.nav6-head{width:100%;display:flex;justify-content:space-between;align-items:center;padding:14px;background:none;border:0;color:#e8eaf2;font:600 14px/1.4 system-ui,sans-serif;cursor:pointer}\n.nav6-head span{transition:transform .3s ease}\n.nav6-item.open .nav6-head span{transform:rotate(45deg)}\n.nav6-panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows .35s cubic-bezier(.2,.7,.2,1)}\n.nav6-item.open .nav6-panel{grid-template-rows:1fr}\n.nav6-clip{overflow:hidden}\n.nav6-body{padding:0 14px 14px;color:#aeb8d4}\n@media (prefers-reduced-motion:reduce){.nav6-panel,.nav6-head span{transition:none}}",
        "js": "document.querySelectorAll('.nav6-head').forEach(h=>h.addEventListener('click',()=>{\n  const item=h.parentElement,open=item.classList.toggle('open');\n  h.setAttribute('aria-expanded',open);\n}));\nsetTimeout(()=>document.querySelector('.nav6-head').click(),1200);"
      },
      "source": {
        "name": "CSS-Tricks — Animating CSS Grid (How To + Examples)",
        "url": "https://css-tricks.com/animating-css-grid-how-to-examples",
        "domain": "css-tricks.com"
      },
      "performance": "Анимируется layout-свойство grid-template-rows, но только одна панель малого размера; это компромисс ради анимации высоты auto.",
      "a11y": "prefers-reduced-motion: transition:none; кнопки с aria-expanded, состояние читается скринридером.",
      "browser_support": "Chrome 107+, Safari 16.4+, Firefox 110+ (анимация grid-template-rows)",
      "ai_use": "Use for FAQ/accordions: wrap each panel in a grid and transition grid-template-rows 0fr -> 1fr over 350ms cubic-bezier(.2,.7,.2,1); the direct child must be overflow:hidden with no padding.",
      "preview": {
        "scroll": false,
        "mouse": ".nav6-head",
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-tabs-sliding-pill",
      "name": "Tabs Sliding Pill Indicator",
      "name_ru": "Табы со скользящим pill-индикатором",
      "category": "navigation",
      "tags": [
        "tabs",
        "sliding-indicator",
        "pill",
        "segmented-control",
        "measure"
      ],
      "summary": "Белая pill-плашка скользит под активным табом, геометрия считается из offsetLeft/offsetWidth — работает при любой ширине подписей.",
      "effect": "Pill: transform translateX + width за 320мс cubic-bezier(.22,1,.36,1). В демо позиция переключается автоматически каждые 1.6с, клик останавливает автоцикл.",
      "when_to_use": "Переключатели тарифов месяц/год, сегментированные контролы каталога, фильтры «все/новые/популярные».",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo nav7\">\n  <div class=\"nav7-tabs\" role=\"tablist\" aria-label=\"Billing period\">\n    <span class=\"nav7-pill\" aria-hidden=\"true\"></span>\n    <button role=\"tab\" aria-selected=\"true\">Monthly</button>\n    <button role=\"tab\" aria-selected=\"false\">Yearly</button>\n    <button role=\"tab\" aria-selected=\"false\">Lifetime</button>\n  </div>\n  <p class=\"nav7-note\" id=\"nav7-note\">Billed every month. Cancel anytime.</p>\n</div>",
        "css": ".nav7{height:320px;display:grid;place-content:center;gap:18px;background:#0b0d12;color:#e8eaf2;font:500 14px/1.5 system-ui,sans-serif;text-align:center}\n.nav7-tabs{position:relative;display:flex;gap:4px;padding:4px;background:#141a29;border-radius:999px}\n.nav7-tabs button{position:relative;z-index:1;padding:9px 18px;border:0;border-radius:999px;background:none;color:#9aa6c4;font:inherit;cursor:pointer;transition:color .2s ease}\n.nav7-tabs button[aria-selected=true]{color:#0b0d12}\n.nav7-pill{position:absolute;top:4px;bottom:4px;left:0;width:0;border-radius:999px;background:#e8eaf2;transition:transform .32s cubic-bezier(.22,1,.36,1),width .32s cubic-bezier(.22,1,.36,1)}\n.nav7-note{margin:0;color:#9aa6c4}\n@media (prefers-reduced-motion:reduce){.nav7-pill{transition:none}}",
        "js": "const tabs=[...document.querySelectorAll('.nav7-tabs button')],pill=document.querySelector('.nav7-pill');\nconst notes=['Billed every month. Cancel anytime.','Two months free, billed once a year.','Pay once, own it forever.'];\nfunction go(i){\n  tabs.forEach((t,j)=>{t.setAttribute('aria-selected',j===i);\n    if(j===i){pill.style.transform=`translateX(${t.offsetLeft}px)`;pill.style.width=`${t.offsetWidth}px`;}});\n  document.getElementById('nav7-note').textContent=notes[i];\n}\ntabs.forEach((t,i)=>t.addEventListener('click',()=>{go(i);clearInterval(timer)}));\nlet k=0;const timer=setInterval(()=>{k=(k+1)%tabs.length;go(k)},1600);\ngo(0);"
      },
      "source": {
        "name": "web.dev — Building a Tabs component",
        "url": "https://web.dev/articles/building/a-tabs-component",
        "domain": "web.dev"
      },
      "performance": "transform/width — композитор, без layout-триггеров на каждом кадре. Геометрия читается один раз на клик (offsetLeft/offsetWidth).",
      "a11y": "role=tablist/tab + aria-selected; prefers-reduced-motion убирает transition, pill перескакивает мгновенно.",
      "browser_support": "Все современные браузеры",
      "ai_use": "Use for segmented controls: measure the active tab's offsetLeft/offsetWidth and move a pill via transform+width transitions of 320ms cubic-bezier(.22,1,.36,1); snap without transition on resize.",
      "preview": {
        "scroll": false,
        "mouse": ".nav7-tabs button",
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-toast-slide-in-stack",
      "name": "Toast Notification Slide-In Stack",
      "name_ru": "Входящие тосты со стеком",
      "category": "navigation",
      "tags": [
        "toast",
        "notification",
        "stack",
        "slide-in",
        "auto-dismiss"
      ],
      "summary": "Тосты въезжают справа и складываются в стек снизу-справа; каждый сам исчезает через 2.6с с обратной анимацией, максимум три в стеке.",
      "effect": "Вход: translateX(110%) → 0 + fade за 300мс cubic-bezier(.2,.9,.3,1). Выход через 2.6с: обратный уход с margin-bottom -52px, чтобы стек схлопнулся за 250мс.",
      "when_to_use": "Подтверждения действий («сохранено», «приглашение отправлено»), фоновые события, статус экспорта — неблокирующий фидбэк.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo nav8\">\n  <button id=\"nav8-add\" class=\"nav8-btn\">Save changes</button>\n  <div class=\"nav8-stack\" id=\"nav8-stack\" role=\"log\" aria-live=\"polite\" aria-label=\"Notifications\"></div>\n</div>",
        "css": ".nav8{position:relative;height:320px;overflow:hidden;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav8-btn{position:absolute;top:16px;left:16px;padding:10px 16px;border:0;border-radius:10px;background:#4f7cff;color:#fff;cursor:pointer;font:inherit}\n.nav8-stack{position:absolute;right:12px;bottom:12px;display:flex;flex-direction:column;gap:8px;width:240px}\n.nav8-toast{padding:11px 12px;background:#141a29;border:1px solid #26304a;border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.45);animation:nav8-in .3s cubic-bezier(.2,.9,.3,1) both}\n.nav8-toast.is-out{animation:nav8-out .25s ease both}\n.nav8-toast strong{display:block;font-size:13px}\n.nav8-toast small{color:#9aa6c4}\n@keyframes nav8-in{from{opacity:0;transform:translateX(110%)}}\n@keyframes nav8-out{to{opacity:0;transform:translateX(110%);margin-bottom:-52px}}\n@media (prefers-reduced-motion:reduce){.nav8-toast,.nav8-toast.is-out{animation-duration:.01s}}",
        "js": "const stack=document.getElementById('nav8-stack');\nconst msgs=[['Changes saved','Synced to the cloud'],['Invite sent','a.lee@north.io'],['Export ready','brand-kit.zip']];\nlet n=0;\nfunction toast(){\n  const el=document.createElement('div');\n  el.className='nav8-toast';\n  el.innerHTML=`<strong>${msgs[n%3][0]}</strong><small>${msgs[n%3][1]}</small>`;\n  n++;stack.appendChild(el);\n  while(stack.children.length>3)stack.firstChild.remove();\n  setTimeout(()=>{el.classList.add('is-out');setTimeout(()=>el.remove(),260)},2600);\n}\ndocument.getElementById('nav8-add').addEventListener('click',toast);\ntoast();setTimeout(toast,700);setInterval(toast,1600);"
      },
      "source": {
        "name": "web.dev — Building a toast component",
        "url": "https://web.dev/articles/building/a-toast-component",
        "domain": "web.dev"
      },
      "performance": "transform/opacity — композитор. Стек ограничен тремя элементами, старые тосты удаляются из DOM.",
      "a11y": "role=log + aria-live=polite: скринридер озвучивает новые тосты; prefers-reduced-motion сокращает анимацию, не убирая текст.",
      "browser_support": "Все современные браузеры (CSS animations, template literals)",
      "ai_use": "Use for success feedback: append toasts that slide in from translateX(110%) in 300ms, auto-dismiss after 2.6s with the reverse animation, and cap the visible stack at three.",
      "preview": {
        "scroll": false,
        "mouse": "#nav8-add",
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-hamburger-x-morph",
      "name": "Hamburger to X Icon Morph",
      "name_ru": "Морфинг гамбургера в крестик",
      "category": "navigation",
      "tags": [
        "hamburger",
        "icon-morph",
        "toggle",
        "transform",
        "menu-button"
      ],
      "summary": "Три полоски бургера превращаются в крестик: верхняя и нижняя сходятся в центр и поворачиваются на ±45°, средняя сжимается по X.",
      "effect": "Полоски 1 и 3: translateY(±10px) + rotate(±45deg); полоска 2: opacity 0 + scaleX(.3). Всё за 350мс cubic-bezier(.2,.7,.2,1). В демо состояние переключается каждые 2.4с.",
      "when_to_use": "Кнопка мобильного меню в шапке, триггер полноэкранного меню или off-canvas панели.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo nav9\">\n  <button id=\"nav9-btn\" class=\"nav9-burger\" aria-label=\"Toggle menu\" aria-expanded=\"false\">\n    <span></span><span></span><span></span>\n  </button>\n</div>",
        "css": ".nav9{height:320px;display:grid;place-items:center;background:#0b0d12}\n.nav9-burger{display:grid;place-content:center;gap:7px;width:64px;height:64px;background:#141a29;border:1px solid #26304a;border-radius:16px;cursor:pointer}\n.nav9-burger span{display:block;width:28px;height:3px;border-radius:99px;background:#e8eaf2;transition:transform .35s cubic-bezier(.2,.7,.2,1),opacity .2s ease}\n.nav9-burger.is-open span:nth-child(1){transform:translateY(10px) rotate(45deg)}\n.nav9-burger.is-open span:nth-child(2){opacity:0;transform:scaleX(.3)}\n.nav9-burger.is-open span:nth-child(3){transform:translateY(-10px) rotate(-45deg)}\n@media (prefers-reduced-motion:reduce){.nav9-burger span{transition:none}}",
        "js": "const b=document.getElementById('nav9-btn');\nfunction flip(){const on=b.classList.toggle('is-open');b.setAttribute('aria-expanded',on)}\nb.addEventListener('click',flip);\nsetTimeout(flip,1200);\nsetInterval(flip,2400);"
      },
      "source": {
        "name": "LogRocket — CSS transitions: Animating a toggle button",
        "url": "https://blog.logrocket.com/css-transitions-animating-hamburger-menu-button",
        "domain": "blog.logrocket.com"
      },
      "performance": "Только transform/opacity — композитор, нулевой layout-след.",
      "a11y": "prefers-reduced-motion: transition:none — иконка меняется мгновенно; aria-label и aria-expanded на кнопке.",
      "browser_support": "Все современные браузеры (nth-child + transform)",
      "ai_use": "Use for mobile menu buttons: morph 3 bars to an X — bars 1/3 translateY(+/-10px) + rotate(+/-45deg), bar 2 scales to 0, all 350ms cubic-bezier(.2,.7,.2,1).",
      "preview": {
        "scroll": false,
        "mouse": "#nav9-btn",
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-page-overlay-wipe",
      "name": "Page Transition Overlay Wipe",
      "name_ru": "Оверлейный wipe при переходе страницы",
      "category": "navigation",
      "tags": [
        "page-transition",
        "overlay",
        "wipe",
        "clip-path",
        "navigation"
      ],
      "summary": "По клику навигации цветной оверлей накрывает экран, под ним подменяется контент, затем оверлей уходит вверх — классический приём для переходов между страницами.",
      "effect": "Оверлей: clip-path inset(100% 0 0 0) → inset(0) за 450мс cubic-bezier(.76,0,.24,1); подмена текста; уход inset(0) → inset(0 0 100% 0) теми же 450мс.",
      "when_to_use": "Переходы между страницами сайта или полноэкранными секциями лендинга, когда важно скрыть момент загрузки нового контента.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo nav10\">\n  <div class=\"nav10-page\">\n    <span class=\"nav10-kicker\">Home</span>\n    <h2 id=\"nav10-title\">Design systems that ship</h2>\n  </div>\n  <div class=\"nav10-cover\" id=\"nav10-cover\"></div>\n</div>",
        "css": ".nav10{position:relative;height:320px;display:grid;place-items:center;overflow:hidden;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif;text-align:center}\n.nav10-page h2{margin:6px 0 0;font-size:22px}\n.nav10-kicker{color:#7f8db3;font-size:11px;letter-spacing:.14em;text-transform:uppercase}\n.nav10-cover{position:absolute;inset:0;background:#4f7cff;clip-path:inset(100% 0 0 0);transition:clip-path .45s cubic-bezier(.76,0,.24,1)}\n.nav10-cover.in{clip-path:inset(0 0 0 0)}\n.nav10-cover.out{clip-path:inset(0 0 100% 0)}\n@media (prefers-reduced-motion:reduce){.nav10-cover{transition:none}}",
        "js": "const cover=document.getElementById('nav10-cover'),page=document.querySelector('.nav10-page');\nconst pages=[['Home','Design systems that ship'],['About','A studio of nine builders'],['Contact','Start a project today']];\nlet i=0;\nfunction navigate(){\n  cover.classList.remove('out');cover.classList.add('in');\n  setTimeout(()=>{\n    i=(i+1)%pages.length;\n    page.querySelector('.nav10-kicker').textContent=pages[i][0];\n    document.getElementById('nav10-title').textContent=pages[i][1];\n    cover.classList.remove('in');cover.classList.add('out');\n    setTimeout(()=>cover.classList.remove('out'),520);\n  },460);\n}\nsetTimeout(navigate,1200);setInterval(navigate,3000);"
      },
      "source": {
        "name": "Smashing Magazine — Improving User Flow Through Page Transitions",
        "url": "https://www.smashingmagazine.com/2016/07/improving-user-flow-through-page-transitions",
        "domain": "www.smashingmagazine.com"
      },
      "performance": "clip-path по композитору; подмена DOM происходит, пока экран полностью закрыт оверлеем.",
      "a11y": "prefers-reduced-motion: transition:none — контент меняется мгновенно без заливки; переход не блокирует ввод после завершения.",
      "browser_support": "Chrome 55+, Safari 13.1+, Firefox 54+ (анимация clip-path)",
      "ai_use": "Use for page transitions: on link click animate a fullscreen cover clip-path inset(100% -> 0) over 450ms cubic-bezier(.76,0,.24,1), swap content while covered, then animate inset(0 -> 100% up).",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-view-transition-spa",
      "name": "View Transitions API — Same Document (SPA)",
      "name_ru": "View Transitions API — в одном документе (SPA)",
      "category": "navigation",
      "tags": [
        "view-transitions",
        "spa",
        "start-view-transition",
        "cross-fade",
        "navigation"
      ],
      "summary": "Смена состояния внутри одной страницы через document.startViewTransition(): браузер сам снимает снапшоты и кроссфейдит карточку с лёгким сдвигом.",
      "effect": "startViewTransition(() => swapDOM) — дефолтный кроссфейд, поверх которого ::view-transition-old/new(nav11-card) анимируются fade + translateY(-10px)/(12px) за 300–350мс.",
      "when_to_use": "SPA-навигация на лендинге: переключение разделов, табов страницы, карточек тарифов без перезагрузки.",
      "tech": [
        "css",
        "vanilla-js",
        "view-transitions"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo nav11\">\n  <button id=\"nav11-toggle\" class=\"nav11-btn\">Switch view</button>\n  <div class=\"nav11-view\" id=\"nav11-view\">\n    <h3>Weekly report</h3>\n    <p>Traffic is up 18% week over week.</p>\n  </div>\n</div>",
        "css": ".nav11{height:320px;display:grid;place-items:center;align-content:center;gap:14px;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav11-btn{padding:9px 16px;border:0;border-radius:10px;background:#4f7cff;color:#fff;cursor:pointer;font:inherit}\n.nav11-view{width:min(340px,84vw);padding:18px;background:#141a29;border:1px solid #26304a;border-radius:14px;view-transition-name:nav11-card}\n.nav11-view h3{margin:0 0 6px;font-size:17px}\n.nav11-view p{margin:0;color:#9aa6c4}\n::view-transition-old(nav11-card){animation:nav11-out .3s ease both}\n::view-transition-new(nav11-card){animation:nav11-in .35s cubic-bezier(.2,.7,.2,1) both}\n@keyframes nav11-out{to{opacity:0;transform:translateY(-10px)}}\n@keyframes nav11-in{from{opacity:0;transform:translateY(12px)}}\n@media (prefers-reduced-motion:reduce){::view-transition-old(nav11-card),::view-transition-new(nav11-card){animation-duration:.01s}}",
        "js": "const view=document.getElementById('nav11-view'),btn=document.getElementById('nav11-toggle');\nconst states=[['Revenue report','MRR grew 12% to $48k.'],['Weekly report','Traffic is up 18% week over week.']];\nlet i=0;\nfunction swap(){\n  i=(i+1)%states.length;\n  view.querySelector('h3').textContent=states[i][0];\n  view.querySelector('p').textContent=states[i][1];\n}\nbtn.addEventListener('click',()=>{document.startViewTransition?document.startViewTransition(swap):swap()});\nsetTimeout(()=>btn.click(),1200);setInterval(()=>btn.click(),2400);"
      },
      "source": {
        "name": "Chrome for Developers — Same-document view transitions for SPAs",
        "url": "https://developer.chrome.com/docs/web-platform/view-transitions/same-document",
        "domain": "developer.chrome.com"
      },
      "performance": "Снапшоты делает браузер; дополнительные анимации только на transform/opacity. Есть фолбэк для браузеров без API.",
      "a11y": "prefers-reduced-motion: длительность анимаций 0.01s (снапшот-переход практически мгновенный); фолбэк без startViewTransition не ломает смену контента.",
      "browser_support": "Chrome 111+, Safari 18+, Firefox 144+ (same-document view transitions)",
      "ai_use": "Use for SPA view changes: call document.startViewTransition(updateDOM) with a feature check, give the changing element a view-transition-name, and animate ::view-transition-old/new with a fade + 12px slide over 300ms.",
      "preview": {
        "scroll": false,
        "mouse": "#nav11-toggle",
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-view-transition-mpa",
      "name": "View Transitions API — Cross-Document (MPA)",
      "name_ru": "View Transitions API — между документами (MPA)",
      "category": "navigation",
      "tags": [
        "view-transitions",
        "mpa",
        "cross-document",
        "slide",
        "navigation"
      ],
      "summary": "Переход между двумя документами: @view-transition { navigation: auto } включает анимированный переход, страницы едут вверх-вниз без JS.",
      "effect": "Реальный MPA-переход требует @view-transition на обеих страницах одного origin; ::view-transition-old(root) уходит translateY(-100%), new(root) приезжает с translateY(100%) за 600мс cubic-bezier(.76,0,.24,1). Демо симулирует эту пару анимаций в одном документе.",
      "when_to_use": "Классические многостраничные сайты и лендинги без SPA: навигация между страницами получает нативный app-like переход одним CSS-правилом.",
      "tech": [
        "css",
        "view-transitions"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo nav12\">\n  <div class=\"nav12-track\" id=\"nav12-track\">\n    <section class=\"nav12-page\"><h3>Projects</h3><p>12 active boards</p></section>\n    <section class=\"nav12-page nav12-alt\"><h3>Notes</h3><p>Shared with 4 people</p></section>\n  </div>\n</div>",
        "css": ".nav12{height:320px;overflow:hidden;background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav12-track{display:grid;grid-template-rows:1fr 1fr;height:200%;transition:transform .6s cubic-bezier(.76,0,.24,1)}\n.nav12-track.next{transform:translateY(-50%)}\n.nav12-track.reset{transition:none}\n.nav12-page{display:grid;align-content:center;justify-items:center;gap:6px}\n.nav12-page h3{margin:0;font-size:22px}\n.nav12-page p{margin:0;color:#9aa6c4}\n.nav12-alt{background:#1d2740}\n@media (prefers-reduced-motion:no-preference){@view-transition{navigation:auto}}\n@media (prefers-reduced-motion:reduce){.nav12-track{transition:none}}",
        "js": "const track=document.getElementById('nav12-track');\nfunction nav(){\n  track.classList.remove('reset');\n  track.classList.add('next');\n  setTimeout(()=>{track.classList.add('reset');track.classList.remove('next')},1500);\n}\nsetTimeout(nav,1200);setInterval(nav,3000);"
      },
      "source": {
        "name": "Chrome for Developers — Cross-document view transitions for MPAs",
        "url": "https://developer.chrome.com/docs/web-platform/view-transitions/cross-document",
        "domain": "developer.chrome.com"
      },
      "performance": "Реальный переход рисует сам браузер (скриншоты + CSS-анимации); в демо — единственный transform на треке.",
      "a11y": "prefers-reduced-motion: и правило @view-transition, и демо-анимация отключены — навигация происходит без движения.",
      "browser_support": "Chrome 126+, Edge 126+, Safari 18.2+; Firefox не поддерживает cross-document (нужен фолбэк-кроссфейд)",
      "ai_use": "For MPAs add @view-transition { navigation: auto } to both same-origin documents and animate ::view-transition-old(root)/::view-transition-new(root) with translateY slides over 600ms; no JS needed.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2400,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-header-hide-on-scroll",
      "name": "Sticky Header Hide on Scroll Down",
      "name_ru": "Sticky-хедер: скрытие при скролле вниз",
      "category": "navigation",
      "tags": [
        "sticky-header",
        "scroll-direction",
        "hide-on-scroll",
        "transform",
        "navigation"
      ],
      "summary": "Шапка уезжает вверх при скролле вниз и возвращается при скролле вверх; у самого верха страницы она всегда видима.",
      "effect": "При скролле вниз (после 80px, дельта > 4px) добавляется .hide → translateY(-110%) за 300мс cubic-bezier(.2,.7,.2,1); при скролле вверх класс снимается.",
      "when_to_use": "Длинные лендинги: хедер с CTA не мешает чтению вниз, но мгновенно доступен при подъёме вверх.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<header class=\"nav13-bar\" id=\"nav13-bar\">\n  <span class=\"nav13-logo\">Fjord</span>\n  <nav><a href=\"#\">Docs</a><a href=\"#\">Pricing</a></nav>\n</header>\n<main class=\"nav13-main\">\n  <section><h3>Build faster</h3><p>Ship your next landing in a day.</p></section>\n  <section><h3>Stay stable</h3><p>Zero-config deploys and rollbacks.</p></section>\n  <section><h3>Scale quietly</h3><p>Edge caching on six continents.</p></section>\n</main>",
        "css": ".nav13-bar{position:sticky;top:0;z-index:2;display:flex;justify-content:space-between;align-items:center;padding:14px 16px;background:#141a29;color:#e8eaf2;font:600 14px/1.4 system-ui,sans-serif;transition:transform .3s cubic-bezier(.2,.7,.2,1);will-change:transform}\n.nav13-bar.hide{transform:translateY(-110%)}\n.nav13-bar a{margin-left:14px;color:#9aa6c4;text-decoration:none;font-weight:500}\n.nav13-main{background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav13-main section{min-height:85vh;padding:28px 20px;border-bottom:1px solid #1b2233}\n.nav13-main h3{margin:0 0 6px;font-size:20px}\n.nav13-main p{margin:0;color:#9aa6c4}\n@media (prefers-reduced-motion:reduce){.nav13-bar{transition:none}}",
        "js": "const bar=document.getElementById('nav13-bar');\nlet last=0;\naddEventListener('scroll',()=>{\n  const y=scrollY;\n  if(Math.abs(y-last)<4)return;\n  bar.classList.toggle('hide',y>last&&y>80);\n  last=y;\n},{passive:true});"
      },
      "source": {
        "name": "Envato Tuts+ — How to Hide/Reveal a Sticky Header on Scroll",
        "url": "https://webdesign.tutsplus.com/how-to-hide-reveal-a-sticky-header-on-scroll-with-javascript--cms-33756t",
        "domain": "webdesign.tutsplus.com"
      },
      "performance": "Только transform, обновление — в passive-обработчике scroll без таймеров; width/height не читаются в горячем пути.",
      "a11y": "prefers-reduced-motion: transition:none — хедер переключается мгновенно; шапка не исчезает у верха страницы.",
      "browser_support": "Все современные браузеры (position:sticky, passive listeners)",
      "ai_use": "Use for long pages: on window scroll with a >4px delta and scrollY > 80, toggle .hide (translateY(-110%)) when scrolling down and remove it when scrolling up; 300ms transition.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "nav-back-to-top-fab",
      "name": "Back-to-Top FAB on Scroll",
      "name_ru": "Кнопка «наверх» при скролле",
      "category": "navigation",
      "tags": [
        "back-to-top",
        "fab",
        "scroll",
        "scroll-behavior",
        "button"
      ],
      "summary": "Круглая кнопка появляется в правом нижнем углу после 140px скролла и плавно возвращает наверх по клику.",
      "effect": "Появление: opacity 0→1 и translateY(14px → 0) за 300мс cubic-bezier(.2,.7,.2,1); клик вызывает scrollTo({top:0,behavior:'smooth'}) с учётом prefers-reduced-motion.",
      "when_to_use": "Длинные страницы с ценами, FAQ, документацией — быстрый возврат к шапке/CTA.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<button class=\"nav14-fab\" id=\"nav14-fab\" aria-label=\"Back to top\">↑</button>\n<main class=\"nav14-content\">\n  <section><h3>Why teams switch</h3><p>Lower latency, fewer refunds.</p></section>\n  <section><h3>Made for scale</h3><p>From 10 to 10 million requests.</p></section>\n  <section><h3>Try it free</h3><p>No card required for 14 days.</p></section>\n</main>",
        "css": ":root{scroll-behavior:smooth}\n.nav14-fab{position:fixed;right:16px;bottom:16px;z-index:3;width:46px;height:46px;border:0;border-radius:50%;background:#4f7cff;color:#fff;font-size:18px;cursor:pointer;opacity:0;transform:translateY(14px);pointer-events:none;transition:opacity .3s ease,transform .3s cubic-bezier(.2,.7,.2,1)}\n.nav14-fab.show{opacity:1;transform:none;pointer-events:auto}\n.nav14-content{background:#0b0d12;color:#e8eaf2;font:14px/1.5 system-ui,sans-serif}\n.nav14-content section{min-height:85vh;padding:28px 20px;border-bottom:1px solid #1b2233}\n.nav14-content h3{margin:0 0 6px;font-size:20px}\n.nav14-content p{margin:0;color:#9aa6c4}\n@media (prefers-reduced-motion:reduce){.nav14-fab{transition:none}:root{scroll-behavior:auto}}",
        "js": "const fab=document.getElementById('nav14-fab');\nconst calm=matchMedia('(prefers-reduced-motion: reduce)').matches;\nfunction sync(){fab.classList.toggle('show',scrollY>140)}\naddEventListener('scroll',sync,{passive:true});sync();\nfab.addEventListener('click',()=>scrollTo({top:0,behavior:calm?'auto':'smooth'}));"
      },
      "source": {
        "name": "MDN — Window: scrollTo() method",
        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo",
        "domain": "developer.mozilla.org"
      },
      "performance": "Fixed-элемент с opacity/transform — композитор, не влияет на layout документа; scroll-обработчик passive и без чтения размеров.",
      "a11y": "aria-label на кнопке; prefers-reduced-motion отключает transition и заменяет плавный скролл на мгновенный (behavior:'auto').",
      "browser_support": "Все современные браузеры (scrollTo с behavior, matchMedia)",
      "ai_use": "Place a fixed FAB bottom-right; toggle .show when scrollY > 140 (opacity + 14px rise over 300ms) and on click scrollTo({top:0,behavior:'smooth'}) unless prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-05-nav.json"
    },
    {
      "id": "bg-gradient-shift",
      "name": "Animated Gradient Background",
      "name_ru": "Переливающийся градиент",
      "category": "background",
      "tags": [
        "gradient",
        "hero",
        "css-only",
        "loop",
        "background-position"
      ],
      "summary": "Фон hero-секции, в котором линейный градиент из трёх цветов медленно перетекает по диагонали: фоновое изображение растянуто до 300% и позиция ходит туда-обратно, поэтому цвета всё время смещаются.",
      "effect": "background-position анимируется 0%→100%→0% за 14s ease-in-out infinite; слой linear-gradient(120deg, #0ea5e9→#6366f1→#8b5cf6→#ec4899) с background-size: 300% 300%, сверху радиальный блик 340px, который едет в той же фазе. Стартует сразу при загрузке, без JS.",
      "when_to_use": "Hero SaaS-лендинга, баннер, CTA-блок, фон ценовой карточки — когда нет фото и нужен живой «дышащий» фон.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo hero\">\n  <h1>Ship faster</h1>\n  <p>Animated gradient background</p>\n</div>",
        "css": ".demo {\n  display: grid; place-items: center; align-content: center; gap: 6px;\n  height: 320px; overflow: hidden; color: #fff; text-align: center;\n  background-image:\n    radial-gradient(340px at 18% 12%, rgba(255, 255, 255, 0.28), transparent 65%),\n    linear-gradient(120deg, #0ea5e9, #6366f1 35%, #8b5cf6 60%, #ec4899);\n  background-size: 120% 120%, 300% 300%;\n  animation: bg-pan 14s ease-in-out infinite;\n}\n.demo h1 { margin: 0; font: 700 34px/1.1 system-ui, sans-serif; }\n.demo p { margin: 0; opacity: 0.85; font: 15px/1.4 system-ui, sans-serif; }\n@keyframes bg-pan {\n  0%, 100% { background-position: 0% 0%, 0% 50%; }\n  50%      { background-position: 100% 100%, 100% 50%; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .demo { animation: none; background-position: 50% 50%, 50% 50%; }\n}",
        "js": ""
      },
      "source": {
        "name": "MDN — background-position (animatable repeatable list)",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/background-position",
        "domain": "developer.mozilla.org"
      },
      "performance": "Анимация background-position перерисовывает слой (paint) каждый кадр; для одного hero-блока дёшево, на весь экран в длинном скролле — дорого.",
      "a11y": "prefers-reduced-motion: reduce — анимация выключается, градиент фиксируется по центру (background-position 50%).",
      "browser_support": "Chrome 90+, Safari 15+, Firefox 90+ (базовая анимация background-position)",
      "ai_use": "Use as hero/section background: 120deg 3-color gradient with background-size 300% 300%, animate background-position 0%->100%->0% over 14s ease-in-out infinite; freeze at 50% under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-conic-aurora",
      "name": "Conic Aurora Spotlight",
      "name_ru": "Аврора на conic-gradient",
      "category": "background",
      "tags": [
        "conic-gradient",
        "aurora",
        "glow",
        "css-only",
        "property"
      ],
      "summary": "Вращающееся полярное сияние: круг конусного градиента с пятью цветами медленно поворачивается за большим blur, создавая живое цветное свечение за контентом.",
      "effect": "Зарегистрированная @property --spin крутится 0deg→360deg за 12s linear infinite; conic-gradient(from var(--spin)) размыт filter: blur(64px) saturate(1.15) и прикрыт радиальной виньеткой (transparent 32% → фон), чтобы центр оставался читаемым.",
      "when_to_use": "Тёмный hero (AI, крипто, devtools), фон модалки, экран пустого состояния, прелоадер — там, где нужна «магия» без картинок.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo aurora\" aria-label=\"Aurora background demo\">\n  <h1>Aurora hero</h1>\n</div>",
        "css": "@property --spin { syntax: \"<angle>\"; inherits: false; initial-value: 0deg; }\n\n.demo {\n  position: relative; display: grid; place-items: center; height: 320px;\n  overflow: hidden; background: #05060f; color: #fff;\n}\n.aurora::before {\n  content: \"\"; position: absolute; inset: -35%;\n  background: conic-gradient(from var(--spin),\n    #6366f1, #22d3ee, #a855f7, #f472b6, #facc15, #6366f1);\n  filter: blur(64px) saturate(1.15);\n  opacity: 0.8;\n  animation: spin 12s linear infinite;\n}\n.aurora::after {\n  content: \"\"; position: absolute; inset: 0;\n  background: radial-gradient(58% 58% at 50% 50%, transparent 32%, #05060f 100%);\n}\n.aurora h1 { position: relative; z-index: 1; font: 700 34px/1.1 system-ui, sans-serif; }\n@keyframes spin { to { --spin: 360deg; } }\n@media (prefers-reduced-motion: reduce) {\n  .aurora::before { animation: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks Almanac — conic-gradient() (animating a registered angle)",
        "url": "https://css-tricks.com/almanac/functions/c/conic-gradient/",
        "domain": "css-tricks.com"
      },
      "performance": "Каждый кадр перерисовывается большой blur-слой (inset: -35%); держите один такой фон на страницу и не анимируйте сам blur.",
      "a11y": "prefers-reduced-motion: reduce — вращение --spin отключается, сияние остаётся статичным; декор на псевдоэлементах не попадает в дерево доступности.",
      "browser_support": "Chrome 85+, Safari 16.4+, Firefox 128+ (@property + conic-gradient)",
      "ai_use": "Place behind dark hero content: full-bleed pseudo-element with conic-gradient using a registered --spin angle, blur(64px), rotating 360deg per 12s linear, vignette on top; stop rotation under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#05060f"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-mesh-blobs",
      "name": "Mesh Gradient Blobs",
      "name_ru": "Меш-градиент из блобов",
      "category": "background",
      "tags": [
        "mesh-gradient",
        "blur",
        "blobs",
        "loop",
        "hero"
      ],
      "summary": "Современный mesh-градиент: четыре цветных круга дрейфуют и масштабируются внутри размытого слоя, цвета плавно перетекают друг в друга, как в обложках Linear и Stripe.",
      "effect": "Четыре круглых radial-блоба (оранжевый, фиолетовый, циан, розовый) едут по контейнеру с inset: -20% разными скоростями: transform: translate + scale(1.25) за 14–20s ease-in-out alternate; общий filter: blur(70px) смешивает края. JS не нужен.",
      "when_to_use": "Hero продуктового лендинга, секция с ценами, фоновый слой под стеклянными карточками (glassmorphism).",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo mesh\">\n  <div class=\"mesh__blobs\" aria-hidden=\"true\"><i></i><i></i><i></i><i></i></div>\n  <h1>Mesh gradient</h1>\n</div>",
        "css": ".demo {\n  position: relative; display: grid; place-items: center; height: 320px;\n  overflow: hidden; background: #0b1020; color: #fff;\n}\n.mesh__blobs { position: absolute; inset: -20%; filter: blur(70px); }\n.mesh__blobs i { position: absolute; width: 62%; aspect-ratio: 1; border-radius: 50%; opacity: 0.8; }\n.mesh__blobs i:nth-child(1) { left: -8%; top: -12%; background: #f97316; --tx: 46%; --ty: 34%; animation: drift 14s ease-in-out infinite alternate; }\n.mesh__blobs i:nth-child(2) { right: -10%; top: 8%; background: #8b5cf6; --tx: -38%; --ty: 42%; animation: drift 18s ease-in-out infinite alternate-reverse; }\n.mesh__blobs i:nth-child(3) { left: 12%; bottom: -22%; background: #06b6d4; --tx: 30%; --ty: -36%; animation: drift 16s ease-in-out infinite alternate; }\n.mesh__blobs i:nth-child(4) { right: 8%; bottom: -16%; background: #ec4899; --tx: -26%; --ty: -30%; animation: drift 20s ease-in-out infinite alternate-reverse; }\n@keyframes drift { to { transform: translate(var(--tx), var(--ty)) scale(1.25); } }\n.demo h1 { position: relative; font: 700 34px/1.1 system-ui, sans-serif; }\n@media (prefers-reduced-motion: reduce) {\n  .mesh__blobs i { animation: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "Quackit — Animated Mesh Gradient (blurred orbs)",
        "url": "https://www.quackit.com/css/animations/examples/css_animation_mesh_gradient.cfm",
        "domain": "www.quackit.com"
      },
      "performance": "Один большой статичный filter: blur(70px) дорого растрируется, но дальше дёшев; анимируются только transform у блобов (композитор).",
      "a11y": "prefers-reduced-motion: reduce — дрейф блобов отключается, остаётся статичный меш-градиент; слой блобов aria-hidden.",
      "browser_support": "Chrome 76+, Safari 9+, Firefox 103+ (filter: blur + CSS variables)",
      "ai_use": "Use as ambient hero background: 4 absolutely-positioned circles with solid colors inside a heavily blurred wrapper (blur ~70px), each drifting via translate+scale keyframes 14-20s ease-in-out alternate; freeze under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b1020"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-blob-morph",
      "name": "Morphing Blob Shape",
      "name_ru": "Морфящийся блоб",
      "category": "background",
      "tags": [
        "blob",
        "border-radius",
        "morph",
        "css-only",
        "loop"
      ],
      "summary": "Органическая фигура непрерывно меняет силуэт: четыре разных профиля border-radius плавно перетекают друг в друга с лёгким вращением — живая «капля» для декора.",
      "effect": "Ключевые кадры 0/25/50/75/100% меняют border-radius между профилями (63% 37% 54% 46% / 55% 48% 52% 45% и т.п.) за 9s ease-in-out infinite; на 50% добавочный rotate(180deg). Заливка — linear-gradient(135deg, cyan→indigo→violet) и мягкая цветная тень.",
      "when_to_use": "Декор hero, акцент за скриншотом продукта, аватар-заглушка, фоновый элемент секции «о нас».",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo blob-stage\">\n  <div class=\"blob\" aria-hidden=\"true\"></div>\n  <h1>Morphing blob</h1>\n</div>",
        "css": ".demo {\n  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;\n  height: 320px; overflow: hidden; background: #0b0d12;\n}\n.blob {\n  width: 200px; height: 200px;\n  background: linear-gradient(135deg, #22d3ee, #6366f1 55%, #a855f7);\n  border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%;\n  box-shadow: 0 18px 60px rgba(99, 102, 241, 0.35);\n  animation: morph 9s ease-in-out infinite;\n}\n.demo h1 { margin: 0; color: #e5e7eb; font: 600 20px/1 system-ui, sans-serif; }\n@keyframes morph {\n  0%, 100% { border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%; transform: rotate(0deg); }\n  25%      { border-radius: 40% 60% 35% 65% / 60% 30% 70% 40%; }\n  50%      { border-radius: 55% 45% 65% 35% / 35% 65% 45% 55%; transform: rotate(180deg); }\n  75%      { border-radius: 30% 70% 50% 50% / 40% 60% 70% 30%; }\n}\n@media (prefers-reduced-motion: reduce) { .blob { animation: none; } }",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks — CSS Blob Recipes (border-radius morph)",
        "url": "https://css-tricks.com/css-blob-recipes/",
        "domain": "css-tricks.com"
      },
      "performance": "border-radius — paint-свойство, но на одном элементе 200×200 дёшево; rotation идёт на композиторе.",
      "a11y": "prefers-reduced-motion: reduce — морфинг отключается, блоб остаётся в базовом органическом силуэте; элемент декоративный (aria-hidden).",
      "browser_support": "Chrome 60+, Safari 10+, Firefox 60+ (слэш-синтаксис border-radius)",
      "ai_use": "Use as decorative hero accent: one 200px square with gradient fill, animate border-radius through 4 asymmetric slash-profiles over 9s ease-in-out infinite with a 180deg rotation at the midpoint; freeze under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2250,
        "bg": "#0b0d12"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-gooey-blobs",
      "name": "Gooey Metaball Blobs",
      "name_ru": "Метаболлы (gooey)",
      "category": "background",
      "tags": [
        "gooey",
        "metaball",
        "svg-filter",
        "gaussian-blur",
        "loop"
      ],
      "summary": "Три круга сближаются и расходятся, а SVG-фильтр сплавляет их в одну липкую массу, как капли ртути: края тянутся и сливаются воедино.",
      "effect": "Фильтр на общем родителе: feGaussianBlur stdDeviation=10 размывает альфу, feColorMatrix (alpha ×18 −7) возвращает жёсткий край, feBlend накладывает оригинал. Круги ездят transform-анимациями 4–5s ease-in-out alternate, поэтому перемычки между ними то появляются, то рвутся.",
      "when_to_use": "Декор тёмного hero (AI, био, крипто), логотип-анимация, фон блока с «жидким» стеклом.",
      "tech": [
        "css",
        "svg"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo gooey\">\n  <svg class=\"gooey__filter\" aria-hidden=\"true\">\n    <defs>\n      <filter id=\"bg-goo\">\n        <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"10\" result=\"blur\" />\n        <feColorMatrix in=\"blur\" type=\"matrix\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7\" result=\"goo\" />\n        <feBlend in=\"SourceGraphic\" in2=\"goo\" />\n      </filter>\n    </defs>\n  </svg>\n  <div class=\"gooey__layer\" aria-hidden=\"true\"><i></i><i></i><i></i></div>\n  <h1>Gooey blobs</h1>\n</div>",
        "css": ".demo {\n  position: relative; display: grid; place-items: center; height: 320px;\n  overflow: hidden; background: #08111f; color: #fff;\n}\n.gooey__filter { position: absolute; width: 0; height: 0; }\n.gooey__layer { position: absolute; inset: 0; filter: url(#bg-goo); }\n.gooey__layer i { position: absolute; top: 42%; width: 19%; aspect-ratio: 1; border-radius: 50%; background: #34d399; }\n.gooey__layer i:nth-child(1) { left: 30%; animation: pull-a 4s ease-in-out infinite alternate; }\n.gooey__layer i:nth-child(2) { left: 52%; background: #22d3ee; animation: pull-b 4s ease-in-out infinite alternate; }\n.gooey__layer i:nth-child(3) { left: 41%; top: 14%; width: 13%; background: #a78bfa; animation: pull-c 5s ease-in-out infinite alternate; }\n@keyframes pull-a { to { transform: translate(55%, 0); } }\n@keyframes pull-b { to { transform: translate(-55%, 0); } }\n@keyframes pull-c { to { transform: translateY(120%); } }\n.demo h1 { position: absolute; bottom: 20px; margin: 0; font: 700 30px/1.1 system-ui, sans-serif; }\n@media (prefers-reduced-motion: reduce) { .gooey__layer i { animation: none; } }",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks — The Gooey Effect (feGaussianBlur + feColorMatrix)",
        "url": "https://css-tricks.com/gooey-effect/",
        "domain": "css-tricks.com"
      },
      "performance": "Фильтр перерасчитывается на каждом кадре движения — держите ≤6 фигур и не растягивайте слой на весь вьюпорт; blur+matrix на 3–4 элементах безопасно.",
      "a11y": "prefers-reduced-motion: reduce — движение кругов останавливается, остаётся статичная слитая фигура (фильтр не убираем — это визуальная суть эффекта); SVG и слой блобов aria-hidden.",
      "browser_support": "Chrome 90+, Safari 15+, Firefox 90+ (inline SVG filter по url(#id))",
      "ai_use": "Use as decorative dark-hero accent: apply one gooey SVG filter (feGaussianBlur stdDeviation 10 + feColorMatrix alpha x18 -7 + feBlend) to a shared parent layer holding 3 colored circles, animate their transforms 4-5s alternate so they merge and split; freeze under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#08111f"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-noise-grain",
      "name": "Animated Grain Overlay",
      "name_ru": "Анимированное зерно",
      "category": "background",
      "tags": [
        "noise",
        "grain",
        "turbulence",
        "texture",
        "overlay"
      ],
      "summary": "Поверх градиента лежит плёночное зерно: сгенерированный SVG-шум дёргается рывками по случайным смещениям, придавая фону фактуру старой плёнки и убирая бандинг градиента.",
      "effect": "Слой 300×300% с data-URI SVG: feTurbulence (fractalNoise, baseFrequency 0.9, stitchTiles) + feColorMatrix saturate(0) и принудительная alpha=1 — ровное серое зерно; тайл 240px повторяется бесшовно, opacity 0.5, mix-blend-mode: overlay, прыжки по 5 позициям animation: grain 1.2s steps(6) infinite.",
      "when_to_use": "Поверх любых градиентов и ярких фонов hero, поверх фото-подложек, в тёмных темах — везде, где нужен «ретро/премиум» налёт.",
      "tech": [
        "css",
        "svg"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo grain-stage\">\n  <div class=\"grain\" aria-hidden=\"true\"></div>\n  <h1>Film grain</h1>\n</div>",
        "css": ".demo {\n  position: relative; display: grid; place-items: center; height: 320px;\n  overflow: hidden; color: #fff;\n  background: linear-gradient(120deg, #1e1b4b, #7c3aed 50%, #db2777);\n}\n.grain {\n  position: absolute; inset: -100%; width: 300%; height: 300%; pointer-events: none;\n  opacity: 0.5; mix-blend-mode: overlay; background-size: 240px 240px;\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 250 250'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0' intercept='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\");\n  animation: grain 1.2s steps(6) infinite;\n}\n.demo h1 { position: relative; font: 700 32px/1.1 system-ui, sans-serif; }\n@keyframes grain {\n  0%, 100% { transform: translate(0, 0); }\n  20% { transform: translate(-6%, 4%); }\n  40% { transform: translate(4%, -7%); }\n  60% { transform: translate(-3%, 6%); }\n  80% { transform: translate(7%, -3%); }\n}\n@media (prefers-reduced-motion: reduce) { .grain { animation: none; } }",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks — Grainy Gradients (feTurbulence as data-URI background)",
        "url": "https://css-tricks.com/grainy-gradients/",
        "domain": "css-tricks.com"
      },
      "performance": "Шаги только по transform — идёт по композитору; SVG-шум зашит в data-URI, сеть не грузится. mix-blend-mode + opacity на большом слое чуть дороже, оставляйте opacity ≤ .25.",
      "a11y": "prefers-reduced-motion: reduce — дёрганье зерна отключается, текстура остаётся статичной; слой декоративный, pointer-events: none, aria-hidden.",
      "browser_support": "Chrome 76+, Safari 15+, Firefox 103+ (mix-blend-mode + data-URI SVG noise)",
      "ai_use": "Overlay on gradient or photo backgrounds: full-bleed layer with data-URI feTurbulence fractalNoise (baseFrequency ~0.9), opacity ~0.2, mix-blend-mode overlay, jittered via steps() transform keyframes 1.2s infinite; static under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1500,
        "bg": "#1e1b4b"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-canvas-dots",
      "name": "Connected Dots Canvas",
      "name_ru": "Частицы с линиями",
      "category": "background",
      "tags": [
        "canvas",
        "particles",
        "connected-dots",
        "vanilla-js",
        "loop"
      ],
      "summary": "Поле частиц на canvas: 42 точки дрейфуют по фону, а между парами, оказавшимися ближе 110px, прорисовываются затухающие линии — классическая «сеть» для tech-лендингов.",
      "effect": "requestAnimationFrame-цикл: точки двигаются со скоростью до 0.6px/кадр и отражаются от краёв; попарные расстояния < 110px дают линию с альфой (1 − d/110) × 0.5. Canvas подгоняется под контейнер по clientWidth/Height.",
      "when_to_use": "Тёмный hero tech/SaaS, фон секции «инфраструктура», заставка блога или презентации.",
      "tech": [
        "vanilla-js",
        "canvas"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"demo dots\">\n  <canvas id=\"bg-dots\"></canvas>\n  <h1>Connected dots</h1>\n</div>",
        "css": ".demo { position: relative; height: 320px; overflow: hidden; background: #070a14; color: #fff; }\n#bg-dots { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n.demo h1 { position: absolute; left: 24px; bottom: 18px; margin: 0; font: 600 20px/1 system-ui, sans-serif; opacity: 0.9; }\n@media (prefers-reduced-motion: reduce) {\n  #bg-dots { display: none; }\n  .demo { background-image: radial-gradient(circle, #2f4f78 1.5px, transparent 1.6px); background-size: 36px 36px; }\n}",
        "js": "const cv = document.getElementById('bg-dots');\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nif (!reduced) {\n  const ctx = cv.getContext('2d');\n  const COUNT = 42, LINK = 110;\n  let points = [];\n\n  function resize() {\n    cv.width = cv.clientWidth; cv.height = cv.clientHeight;\n    points = Array.from({ length: COUNT }, () => ({\n      x: Math.random() * cv.width, y: Math.random() * cv.height,\n      vx: (Math.random() - 0.5) * 0.6, vy: (Math.random() - 0.5) * 0.6,\n    }));\n  }\n\n  function draw() {\n    ctx.clearRect(0, 0, cv.width, cv.height);\n    for (const p of points) {\n      p.x += p.vx; p.y += p.vy;\n      if (p.x < 0 || p.x > cv.width) p.vx *= -1;\n      if (p.y < 0 || p.y > cv.height) p.vy *= -1;\n      ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, 7); ctx.fillStyle = '#7dd3fc'; ctx.fill();\n    }\n    for (let i = 0; i < points.length; i++) {\n      for (let j = i + 1; j < points.length; j++) {\n        const dist = Math.hypot(points[i].x - points[j].x, points[i].y - points[j].y);\n        if (dist < LINK) {\n          ctx.strokeStyle = `rgba(125,211,252,${(1 - dist / LINK) * 0.5})`;\n          ctx.beginPath(); ctx.moveTo(points[i].x, points[i].y);\n          ctx.lineTo(points[j].x, points[j].y); ctx.stroke();\n        }\n      }\n    }\n    requestAnimationFrame(draw);\n  }\n\n  resize(); draw();\n  addEventListener('resize', resize);\n}"
      },
      "source": {
        "name": "MDN — Canvas API: Basic animations",
        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations",
        "domain": "developer.mozilla.org"
      },
      "performance": "O(n²) попарных проверок: 42 точки ≈ 860 пар/кадр — дёшево; держите COUNT ≤ 60. Canvas без retina-скейла и маленького размера.",
      "a11y": "prefers-reduced-motion: reduce — rAF-цикл не запускается вовсе, canvas скрыт, показывается статичный узор из CSS-градиента; текст остаётся в h1.",
      "browser_support": "Chrome 60+, Safari 12+, Firefox 60+ (canvas 2d + rAF)",
      "ai_use": "Use for dark tech heroes: small fixed canvas behind a headline; ~40 particles moving <1px/frame with edge bounce, draw lines between pairs closer than ~110px with alpha fading by distance; under prefers-reduced-motion draw a single static frame (no rAF).",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#070a14"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-tsparticles-links",
      "name": "tsParticles Linked Preset",
      "name_ru": "tsParticles: связанные частицы",
      "category": "background",
      "tags": [
        "tsparticles",
        "particles",
        "cdn",
        "links",
        "loop"
      ],
      "summary": "Лёгкая версия tsParticles (slim-бандл): 36 фиолетовых частиц летают и соединяются линиями; при наведении курсора частицы тянутся к нему (режим grab).",
      "effect": "tsParticles.load('tsparticles', {...}) из slim-бандла: number 36 (density выключена, чтобы счётчик не резался под маленький контейнер), links distance 120 (opacity 0.4), размер 1–3px, скорость 1.2, hover grab. Движок сам держит rAF-цикл и ресайз; fullScreen отключён — canvas живёт внутри демо-блока.",
      "when_to_use": "Hero или фон CTA-секции, когда хочется интерактивную частичную подложку без ручного кода частиц.",
      "tech": [
        "vanilla-js",
        "canvas"
      ],
      "dependencies": [
        "tsparticles-slim 2.12.0 (jsDelivr CDN)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/tsparticles-slim@2.12.0/tsparticles.slim.bundle.min.js\"></script>\n<div class=\"demo tsp\">\n  <div id=\"tsparticles\" class=\"tsp__canvas\" aria-hidden=\"true\"></div>\n  <h1>tsParticles</h1>\n</div>",
        "css": ".demo { position: relative; display: grid; place-items: center; height: 320px; overflow: hidden;\n  background: radial-gradient(120% 100% at 50% 110%, #141b3a, #06070d 70%); color: #fff; }\n.tsp__canvas { position: absolute; inset: 0; }\n.demo h1 { position: relative; font: 700 32px/1.1 system-ui, sans-serif; }\n@media (prefers-reduced-motion: reduce) {\n  .tsp__canvas { opacity: 0.5; background-image: radial-gradient(#8b5cf6 1.5px, transparent 1.6px); background-size: 44px 44px; }\n}",
        "js": "const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nif (!reduced && window.tsParticles) {\n  tsParticles.load('tsparticles', {\n    fullScreen: { enable: false },\n    background: { color: 'transparent' },\n    particles: {\n      number: { value: 36, density: { enable: false } },\n      color: { value: '#8b5cf6' },\n      size: { value: { min: 1, max: 3 } },\n      opacity: { value: 0.7 },\n      links: { enable: true, distance: 120, color: '#8b5cf6', opacity: 0.4, width: 1 },\n      move: { enable: true, speed: 1.2 },\n    },\n    interactivity: { events: { onHover: { enable: true, mode: 'grab' } } },\n  });\n}"
      },
      "source": {
        "name": "tsParticles — official docs (slim bundle, CDN usage)",
        "url": "https://particles.js.org/docs/",
        "domain": "particles.js.org"
      },
      "performance": "Canvas рисуется библиотекой; ограничивайте number.value и не включайте fullScreen, чтобы не перерисовывать весь вьюпорт. Slim-бандл не тянет тяжёлые плагины.",
      "a11y": "prefers-reduced-motion: reduce — tsParticles.load не вызывается вовсе, вместо canvas остаётся статичный узор из CSS-градиента (медиазапрос в CSS).",
      "browser_support": "Chrome 90+, Safari 15+, Firefox 90+ (ES2018, canvas 2d)",
      "ai_use": "Use tsParticles slim bundle from jsDelivr with fullScreen:false inside a positioned container; ~35 particles with links (distance ~120, opacity .4), hover grab; skip init entirely under prefers-reduced-motion and keep a static CSS pattern.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 3000,
        "bg": "#06070d"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-starfield-twinkle",
      "name": "Twinkling Starfield",
      "name_ru": "Мерцающее звёздное небо",
      "category": "background",
      "tags": [
        "stars",
        "twinkle",
        "js-random",
        "css-animation",
        "loop"
      ],
      "summary": "Ночной фон с ~70 звёздами: JS раскидывает их случайно и даёт каждой свою скорость и задержку мерцания, поэтому небо переливается неравномерно, а не мигает хором.",
      "effect": "JS создаёт 70 звёзд-элементов со случайными left/top, размером 2–4px и animation-delay от −3s до 0; CSS-кейфрейм twinkle крутит opacity 0.25→1 и scale 0.7→1.15 за 1.6–4s ease-in-out infinite. Фон — радиальный градиент с синим свечением у нижнего края.",
      "when_to_use": "Hero ночного приложения, фон секции релиза, тёмная тема презентаций, экран «Coming soon».",
      "tech": [
        "vanilla-js",
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo sky\" id=\"bg-starfield\">\n  <h1>Under the stars</h1>\n</div>",
        "css": ".demo { position: relative; display: grid; place-items: center; height: 320px; overflow: hidden; color: #fff;\n  background: radial-gradient(130% 100% at 50% 120%, #1e2a5a, #05060f 70%); }\n.demo h1 { position: relative; font: 700 30px/1.1 system-ui, sans-serif; }\n.star { position: absolute; border-radius: 50%; background: #fff; box-shadow: 0 0 6px #c7d2fe; animation: twinkle 2.4s ease-in-out infinite; }\n@keyframes twinkle { 0%, 100% { opacity: 0.25; transform: scale(0.7); } 50% { opacity: 1; transform: scale(1.15); } }\n@media (prefers-reduced-motion: reduce) { .star { animation: none; opacity: 0.7; } }",
        "js": "const sky = document.getElementById('bg-starfield');\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nfor (let i = 0; i < 70; i++) {\n  const star = document.createElement('i');\n  star.className = 'star';\n  const size = (2 + Math.random() * 2).toFixed(1);\n  star.style.cssText =\n    `left:${(Math.random() * 100).toFixed(2)}%;top:${(Math.random() * 100).toFixed(2)}%;` +\n    `width:${size}px;height:${size}px;` +\n    `animation-duration:${(1.6 + Math.random() * 2.4).toFixed(2)}s;` +\n    `animation-delay:-${(Math.random() * 3).toFixed(2)}s`;\n  if (reduced) star.style.animation = 'none';\n  sky.appendChild(star);\n}"
      },
      "source": {
        "name": "CSS-Tricks — An Interactive Starry Backdrop (random stars + twinkle)",
        "url": "https://css-tricks.com/an-interactive-starry-backdrop-for-content/",
        "domain": "css-tricks.com"
      },
      "performance": "70 мелких элементов, анимируются только opacity и transform — композитор; свечение одним коротким box-shadow, без размытых теней у каждой звезды.",
      "a11y": "prefers-reduced-motion: reduce — мерцание отключается и в CSS, и в JS (animation: none), звёзды остаются видимыми статичными точками opacity .7.",
      "browser_support": "Chrome 60+, Safari 12+, Firefox 60+ (CSS animations + cssText)",
      "ai_use": "Use as dark hero backdrop: JS creates ~70 absolutely-positioned 2-4px dots with random positions, each running an opacity+scale twinkle keyframe with random duration 1.6-4s and negative delay for desync; disable animation under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1500,
        "bg": "#05060f"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-confetti-burst",
      "name": "Confetti Burst",
      "name_ru": "Салют из конфетти",
      "category": "background",
      "tags": [
        "confetti",
        "celebration",
        "canvas",
        "cdn",
        "click"
      ],
      "summary": "Праздничный залп конфетти из трёх точек (центр и два нижних угла) поверх контента — для момента успеха: деплой прошёл, заявка отправлена, цель достигнута.",
      "effect": "canvas-confetti стреляет тремя залпами по 40–80 частиц: spread 70, startVelocity 42, ticks 160 (затухание ~2.5s), палитра из 5 цветов; боковые залпы летят под углами 60° и 120°. Автозапуск при загрузке и каждые 3.2s для превью, плюс по клику на кнопку.",
      "when_to_use": "После успешной оплаты/регистрации, на CTA «готово», в демо-сцене достижения — как краткий акцент, не постоянный фон.",
      "tech": [
        "vanilla-js",
        "canvas"
      ],
      "dependencies": [
        "canvas-confetti 1.9.4 (jsDelivr CDN)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/confetti.browser.min.js\"></script>\n<div class=\"demo party\">\n  <h1>Deploy succeeded</h1>\n  <button id=\"bg-confetti-btn\" type=\"button\">Celebrate again</button>\n</div>",
        "css": ".demo {\n  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;\n  height: 320px; overflow: hidden; color: #fff;\n  background: radial-gradient(120% 100% at 50% 0%, #1c2340, #0b0d12 70%);\n}\n.demo h1 { margin: 0; font: 700 28px/1.1 system-ui, sans-serif; }\n.demo button { padding: 10px 20px; border: 0; border-radius: 999px; cursor: pointer; background: #6366f1; color: #fff; font: 600 15px/1 system-ui, sans-serif; }\n.demo button:focus-visible { outline: 2px solid #a5b4fc; outline-offset: 2px; }\n@media (prefers-reduced-motion: reduce) { .demo button { cursor: default; } }",
        "js": "const btn = document.getElementById('bg-confetti-btn');\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nfunction burst() {\n  if (reduced || typeof confetti !== 'function') return;\n  const base = { spread: 70, startVelocity: 42, ticks: 160,\n    colors: ['#6366f1', '#22d3ee', '#f472b6', '#facc15', '#34d399'] };\n  confetti({ ...base, particleCount: 80, origin: { x: 0.5, y: 0.7 } });\n  confetti({ ...base, particleCount: 40, angle: 60, origin: { x: 0.1, y: 0.8 } });\n  confetti({ ...base, particleCount: 40, angle: 120, origin: { x: 0.9, y: 0.8 } });\n}\n\nbtn.addEventListener('click', burst);\nif (!reduced) { setTimeout(burst, 500); setInterval(burst, 3200); }"
      },
      "source": {
        "name": "GitHub — canvas-confetti (CDN usage)",
        "url": "https://github.com/catdad/canvas-confetti",
        "domain": "github.com"
      },
      "performance": "Библиотека рисует частицы на своём canvas и останавливает rAF, когда частицы догорели; три залпа на всплеск — безопасно.",
      "a11y": "prefers-reduced-motion: reduce — burst() выходит сразу, конфетти не создаётся; кнопка остаётся рабочей и имеет focus-visible.",
      "browser_support": "Chrome 60+, Safari 12+, Firefox 60+ (canvas-confetti 1.9.x)",
      "ai_use": "Fire on success action (button click) and once on load for demos: three canvas-confetti bursts (center + two 60/120deg corners), spread 70, startVelocity 42, ticks 160, brand palette; no-op under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-webgl-shader-gradient",
      "name": "WebGL Shader Gradient",
      "name_ru": "Шейдерный градиент на WebGL",
      "category": "background",
      "tags": [
        "webgl",
        "glsl",
        "shader",
        "gradient",
        "hero"
      ],
      "summary": "Живой градиент, посчитанный во фрагментном шейдере: три цвета смешиваются синусоидальными волнами от координаты пикселя и времени, без единой картинки.",
      "effect": "Raw WebGL 1: один полноэкранный треугольник, во фрагментном шейдере волны sin(uv±uT) смешивают цвета (тёмно-синий → фиолетовый → циан) и дают лёгкое биение; uT обновляется в rAF (~60fps). Компиляция и линковка — на старте, dpr ограничен 1.5.",
      "when_to_use": "Hero без фоновых изображений, промо-блоки, лендинги AI/3D-продуктов — когда нужен «дорогой» программный фон минимального веса.",
      "tech": [
        "webgl",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "hard",
      "code": {
        "html": "<div class=\"demo gl-hero\">\n  <canvas id=\"bg-gl\"></canvas>\n  <h1>Shader gradient</h1>\n</div>",
        "css": ".demo { position: relative; display: grid; place-items: center; height: 320px; overflow: hidden; color: #fff; background: #070a14; }\n#bg-gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n.demo h1 { position: relative; font: 700 32px/1.1 system-ui, sans-serif; text-shadow: 0 2px 24px rgba(0, 0, 0, 0.5); }\n@media (prefers-reduced-motion: reduce) {\n  #bg-gl { display: none; }\n  .demo { background: linear-gradient(130deg, #0b1030, #6b3fd6 55%, #1796b8); }\n}",
        "js": "const cv = document.getElementById('bg-gl');\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst gl = reduced ? null : cv.getContext('webgl', { antialias: false, alpha: false });\n\nif (gl) {\n  const vs = 'attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}';\n  const fs = `precision highp float;uniform float uT;uniform vec2 uR;\n    void main(){\n      vec2 uv = gl_FragCoord.xy / uR; uv.x *= uR.x / uR.y;\n      float w1 = sin(uv.x * 2.1 + uT * 0.6 + uv.y * 1.4) * 0.5 + 0.5;\n      float w2 = sin(uv.y * 2.7 - uT * 0.45 + uv.x * 0.8) * 0.5 + 0.5;\n      vec3 col = mix(vec3(0.05, 0.07, 0.18), vec3(0.42, 0.24, 0.85), w1);\n      col = mix(col, vec3(0.09, 0.68, 0.8), w2 * 0.55);\n      gl_FragColor = vec4(col, 1.0);\n    }`;\n  const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s; };\n  const pr = gl.createProgram();\n  gl.attachShader(pr, sh(gl.VERTEX_SHADER, vs));\n  gl.attachShader(pr, sh(gl.FRAGMENT_SHADER, fs));\n  gl.linkProgram(pr); gl.useProgram(pr);\n  const buf = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n  const loc = gl.getAttribLocation(pr, 'p');\n  gl.enableVertexAttribArray(loc);\n  gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n  const uT = gl.getUniformLocation(pr, 'uT'), uR = gl.getUniformLocation(pr, 'uR');\n\n  function resize() {\n    const w = cv.clientWidth, h = cv.clientHeight, d = Math.min(devicePixelRatio || 1, 1.5);\n    cv.width = w * d; cv.height = h * d;\n    gl.viewport(0, 0, cv.width, cv.height);\n  }\n  function frame(t) {\n    gl.uniform1f(uT, t / 1000);\n    gl.uniform2f(uR, cv.width, cv.height);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n    requestAnimationFrame(frame);\n  }\n  resize(); requestAnimationFrame(frame);\n  addEventListener('resize', resize);\n}"
      },
      "source": {
        "name": "WebGL Fundamentals — WebGL Shadertoy (u_time-driven fragment shader)",
        "url": "https://webglfundamentals.org/webgl/lessons/webgl-shadertoy.html",
        "domain": "webglfundamentals.org"
      },
      "performance": "Один полноэкранный треугольник, простой шейдер без текстур — стабильные 60fps; dpr ограничен 1.5, antialias отключён. При reduced-motion — один кадр без rAF.",
      "a11y": "prefers-reduced-motion: reduce — WebGL-контекст вообще не создаётся, canvas скрыт, фоном служит статичный CSS-градиент; заголовок остаётся в h1.",
      "browser_support": "Chrome 60+, Safari 12+, Firefox 60+ (WebGL 1, GLSL ES 1.0)",
      "ai_use": "Use for image-free hero backgrounds: raw WebGL1 full-screen triangle with a fragment shader using uniform time to mix 3 colors via sine waves; cap devicePixelRatio at 1.5, render one static frame under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#070a14"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-vanta-waves",
      "name": "Vanta.js Waves",
      "name_ru": "Vanta.js: волны",
      "category": "background",
      "tags": [
        "vanta",
        "three-js",
        "webgl",
        "cdn",
        "waves"
      ],
      "summary": "Готовый 3D-фон из волн Vanta.js на three.js: тёмная водная гладь, которая мягко колышется и реагирует на движение мыши — подключается тремя строчками.",
      "effect": "VANTA.WAVES({ waveHeight: 22, waveSpeed: 1.2, zoom: 0.8, shininess: 40, color: 0x0b1030 }) вставляет canvas внутрь блока и рендерит 60fps-волны на WebGL; мышь/тач влияют на рябь. Параметры подобраны под тёмный hero.",
      "when_to_use": "Hero и футер тёмного лендинга, промо-страница конференции, корпоративный сайт — когда нужен «дорогой» 3D-фон без собственных шейдеров.",
      "tech": [
        "webgl",
        "vanilla-js"
      ],
      "dependencies": [
        "three.js r134 (cdnjs) + vanta 0.5.24 (jsDelivr CDN)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/vanta@0.5.24/dist/vanta.waves.min.js\"></script>\n<div class=\"demo vanta\" id=\"bg-vanta\">\n  <h1>Vanta waves</h1>\n</div>",
        "css": ".demo { position: relative; display: grid; place-items: center; height: 320px; overflow: hidden; color: #fff;\n  background: radial-gradient(120% 100% at 50% 120%, #16213e, #05060f 70%); }\n.demo h1 { position: relative; z-index: 1; font: 700 32px/1.1 system-ui, sans-serif; }\n@media (prefers-reduced-motion: reduce) {\n  .demo canvas { display: none; }\n}",
        "js": "const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nif (!reduced && window.VANTA && window.VANTA.WAVES) {\n  VANTA.WAVES({\n    el: '#bg-vanta',\n    color: 0x0b1030,\n    waveHeight: 22,\n    waveSpeed: 1.2,\n    shininess: 40,\n    zoom: 0.8,\n  });\n}"
      },
      "source": {
        "name": "Vanta.js — official site (Waves effect, CDN usage)",
        "url": "https://www.vantajs.com/",
        "domain": "www.vantajs.com"
      },
      "performance": "three.js ~120 КБ gzip + WebGL-канвас размера блока; держите один такой фон на страницу и не накрывайте его тяжёлым backdrop-filter.",
      "a11y": "prefers-reduced-motion: reduce — эффект не инициализируется, канвас скрыт через CSS, остаётся статичный градиент с заголовком.",
      "browser_support": "Chrome 90+, Safari 15+, Firefox 90+ (WebGL + three.js r134)",
      "ai_use": "For a dark hero/footer: include three.js r134 and vanta.waves from CDN, init VANTA.WAVES with dark color, waveHeight ~22, waveSpeed ~1.2, zoom ~0.8; skip init under prefers-reduced-motion and keep a CSS gradient fallback.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 3500,
        "bg": "#05060f"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-marquee-text",
      "name": "Infinite Text Marquee",
      "name_ru": "Бегущая строка",
      "category": "background",
      "tags": [
        "marquee",
        "css-only",
        "translate",
        "loop",
        "logos"
      ],
      "summary": "Бесконечная бегущая строка из названий компаний: содержимое продублировано и уезжает ровно на половину ширины, поэтому шов незаметен, а по наведению строка встаёт на паузу.",
      "effect": "Дорожка из двух одинаковых групп едет translateX(0 → −50%) за 16s linear infinite; дубль скрыт aria-hidden. Края растворяются mask-image градиентом, hover ставит animation-play-state: paused.",
      "when_to_use": "Полоса «нам доверяют» под hero, тикер новостей, лента технологий/интеграций в подвале секции.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo marquee\">\n  <p class=\"marquee__label\">Trusted by 2,000+ teams</p>\n  <div class=\"marquee__viewport\">\n    <div class=\"marquee__track\">\n      <div class=\"marquee__group\">\n        <span>◆ Acme</span><span>● Vertex</span><span>▲ Nimbus</span><span>■ Quanta</span>\n      </div>\n      <div class=\"marquee__group\" aria-hidden=\"true\">\n        <span>◆ Acme</span><span>● Vertex</span><span>▲ Nimbus</span><span>■ Quanta</span>\n      </div>\n    </div>\n  </div>\n</div>",
        "css": ".demo { display: grid; align-content: center; gap: 14px; height: 320px; overflow: hidden; background: #0b0d12; color: #e5e7eb; }\n.marquee__label { margin: 0; padding-left: 24px; font: 500 13px/1 system-ui, sans-serif; letter-spacing: 0.08em; text-transform: uppercase; color: #8b93a7; }\n.marquee__viewport { overflow: hidden;\n  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);\n  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); }\n.marquee__track { display: flex; width: max-content; animation: scroll 16s linear infinite; }\n.marquee__group { display: flex; gap: 40px; padding-right: 40px; font: 600 22px/1 system-ui, sans-serif; white-space: nowrap; }\n.marquee:hover .marquee__track { animation-play-state: paused; }\n@keyframes scroll { to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) { .marquee__track { animation: none; } }",
        "js": ""
      },
      "source": {
        "name": "CSS-Tricks — CSS-Only Marquee Effect",
        "url": "https://css-tricks.com/css-only-marquee-effect/",
        "domain": "css-tricks.com"
      },
      "performance": "transform-only анимация едет по композитору; маска по краям добавляет немного paint. Держите содержимое лёгким (текст, не тяжёлые картинки).",
      "a11y": "prefers-reduced-motion: reduce — движение выключается, строка остаётся статичной; дубль скрыт aria-hidden от скринридеров.",
      "browser_support": "Chrome 90+, Safari 15.4+ (mask-image), Firefox 103+",
      "ai_use": "For a 'trusted by' ribbon: duplicate the item row, animate the track translateX 0 to -50% linear infinite (~16s), mask both edges, pause on hover, aria-hidden on the duplicate; stop animation under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "bg-logo-wall",
      "name": "Dual-Row Logo Wall",
      "name_ru": "Стена логотипов в две строки",
      "category": "background",
      "tags": [
        "logo-wall",
        "marquee",
        "dual-row",
        "css-only",
        "social-proof"
      ],
      "summary": "Социальное доказательство в две дорожки: верхний ряд логотипов едет влево, нижний — вправо, скорости чуть разные, поэтому ряды никогда не синхронизируются; по наведению вся стена замирает.",
      "effect": "Один общий кейфрейм translateX(0 → −50%) на две дорожки: верхняя 24s, нижняя 30s с animation-direction: reverse. Логотипы — чипы с цветной точкой; hover/focus-within ставит обе дорожки на паузу, края смаскированы.",
      "when_to_use": "Секция «Нам доверяют» в середине лендинга, блок клиентов на B2B-сайте, подвал агентства.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"demo wall\">\n  <p class=\"wall__title\">Trusted by 200+ product teams</p>\n  <div class=\"wall__row\">\n    <div class=\"wall__track\">\n      <div class=\"wall__group\"><span class=\"chip\"><i style=\"background:#2563eb\"></i>Northbeam</span><span class=\"chip\"><i style=\"background:#dc2626\"></i>Cartload</span><span class=\"chip\"><i style=\"background:#059669\"></i>Fernwork</span><span class=\"chip\"><i style=\"background:#7c3aed\"></i>Quill</span></div>\n      <div class=\"wall__group\" aria-hidden=\"true\"><span class=\"chip\"><i style=\"background:#2563eb\"></i>Northbeam</span><span class=\"chip\"><i style=\"background:#dc2626\"></i>Cartload</span><span class=\"chip\"><i style=\"background:#059669\"></i>Fernwork</span><span class=\"chip\"><i style=\"background:#7c3aed\"></i>Quill</span></div>\n    </div>\n  </div>\n  <div class=\"wall__row\">\n    <div class=\"wall__track wall__track--rev\">\n      <div class=\"wall__group\"><span class=\"chip\"><i style=\"background:#be185d\"></i>Alpenglow</span><span class=\"chip\"><i style=\"background:#4d7c0f\"></i>Statfield</span><span class=\"chip\"><i style=\"background:#b45309\"></i>Mooring</span><span class=\"chip\"><i style=\"background:#0f766e\"></i>Palisade</span></div>\n      <div class=\"wall__group\" aria-hidden=\"true\"><span class=\"chip\"><i style=\"background:#be185d\"></i>Alpenglow</span><span class=\"chip\"><i style=\"background:#4d7c0f\"></i>Statfield</span><span class=\"chip\"><i style=\"background:#b45309\"></i>Mooring</span><span class=\"chip\"><i style=\"background:#0f766e\"></i>Palisade</span></div>\n    </div>\n  </div>\n</div>",
        "css": ".demo { display: grid; align-content: center; gap: 16px; height: 320px; overflow: hidden;\n  background: radial-gradient(120% 120% at 50% 0%, #131a2e, #0b0d12 70%); color: #e5e7eb; }\n.wall__title { margin: 0 0 4px; text-align: center; font: 500 13px/1 system-ui, sans-serif; letter-spacing: 0.08em; text-transform: uppercase; color: #8b93a7; }\n.wall__row { overflow: hidden;\n  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);\n  mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); }\n.wall__track { display: flex; width: max-content; animation: wall 24s linear infinite; }\n.wall__track--rev { animation-duration: 30s; animation-direction: reverse; }\n.wall__group { display: flex; gap: 14px; padding-right: 14px; }\n.chip { display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border: 1px solid rgba(255, 255, 255, 0.14);\n  border-radius: 12px; background: rgba(255, 255, 255, 0.05); font: 600 14px/1 system-ui, sans-serif; white-space: nowrap; }\n.chip i { width: 8px; height: 8px; border-radius: 50%; }\n.wall:hover .wall__track { animation-play-state: paused; }\n@keyframes wall { to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) { .wall__track { animation: none; } }",
        "js": ""
      },
      "source": {
        "name": "Smashing Magazine — Infinite-Scrolling Logos In Flat HTML And Pure CSS",
        "url": "https://www.smashingmagazine.com/2024/04/infinite-scrolling-logos-html-css/",
        "domain": "www.smashingmagazine.com"
      },
      "performance": "Две transform-анимации + mask — композитор; держите DOM дорожек неглубоким (чипы, не изображения с тенями).",
      "a11y": "prefers-reduced-motion: reduce — обе дорожки останавливаются; дубли групп помечены aria-hidden; hover-пауза работает и для клавиатуры через focus-within (при добавлении ссылок).",
      "browser_support": "Chrome 90+, Safari 15.4+ (mask-image), Firefox 103+",
      "ai_use": "For a mid-page trust section: two marquee rows sharing one translateX 0 to -50% keyframe; top row 24s, bottom row 30s with animation-direction:reverse; pause both on hover/focus-within; duplicate groups aria-hidden; freeze under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-06-bg.json"
    },
    {
      "id": "phys-spring-entrance",
      "name": "Spring Physics Entrance",
      "name_ru": "Пружинное появление без библиотек",
      "category": "physics3d",
      "tags": [
        "on-load",
        "spring",
        "overshoot",
        "css-easing",
        "stagger"
      ],
      "summary": "Hero-блок появляется снизу пружиной: подъём с перелётом и затухающими колебаниями, чисто на CSS linear() — без JS и библиотек.",
      "effect": "Элементы идут с задержкой 50/140/260 мс: translateY(26px) scale(.96) → none, opacity 0→1, длительность 950 мс. Easing — пружина linear() с пиком 109% на 34% времени и колебаниями 100→96→100. В браузерах без linear() — cubic-bezier(.22,1.45,.4,1) через @supports.",
      "when_to_use": "Hero-заголовок, подзаголовок и CTA при первой загрузке; блоки преимуществ и пустые состояния, где нужен «живой», но не кричащий вход.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"spring-hero\">\n  <p class=\"eyebrow\">Nova UI Kit</p>\n  <h1>Ship interfaces that feel alive</h1>\n  <a class=\"spring-btn\" href=\"#\">Get started</a>\n</section>",
        "css": ":root { --spring: linear(0, 0.234 5%, 0.73 15%, 1.045 27%, 1.09 34%, 0.995 50%, 0.965 60%, 1.007 75%, 1.002 87%, 1); }\n.spring-hero { display: grid; gap: 10px; justify-items: start; padding: 34px 26px; font-family: system-ui, sans-serif; color: #eef1ff; }\n.spring-hero > * { margin: 0; opacity: 0; transform: translateY(26px) scale(.96); animation: spring-rise .95s var(--spring) both; }\n.spring-hero .eyebrow { font: 700 11px/1 ui-monospace, monospace; letter-spacing: .2em; text-transform: uppercase; color: #8ea0ff; animation-delay: .05s; }\n.spring-hero h1 { max-width: 320px; font-size: 30px; line-height: 1.12; animation-delay: .14s; }\n.spring-hero .spring-btn { padding: 11px 18px; border-radius: 12px; background: #7c8cff; color: #0b0d12; font-weight: 700; font-size: 14px; text-decoration: none; animation-delay: .26s; }\n@keyframes spring-rise { to { opacity: 1; transform: none; } }\n@supports not (animation-timing-function: linear(0, 1)) {\n  .spring-hero > * { animation-timing-function: cubic-bezier(.22, 1.45, .4, 1); }\n}\n@media (prefers-reduced-motion: reduce) {\n  .spring-hero > * { animation: none; opacity: 1; transform: none; }\n}",
        "js": ""
      },
      "source": {
        "name": "MDN — linear() easing function",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/linear",
        "domain": "developer.mozilla.org"
      },
      "performance": "Только transform/opacity, compositor-only; linear() заметно дешевле пружинных расчётов на JS.",
      "a11y": "prefers-reduced-motion: reduce отключает animation, элементы сразу в конечном состоянии (opacity:1, transform:none).",
      "browser_support": "linear() easing: Chrome 113+, Safari 17.2+, Firefox 112+; ветка @supports с cubic-bezier работает во всех современных.",
      "ai_use": "Apply to hero copy block on load: translateY(26px)+scale(.96) to none, opacity 0 to 1, 950ms CSS linear() spring (peak 109% at 34%), stagger children 90-120ms.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1400,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-drag-inertia",
      "name": "Draggable Element with Inertia",
      "name_ru": "Перетаскивание с инерцией",
      "category": "physics3d",
      "tags": [
        "drag",
        "inertia",
        "momentum",
        "pointer-events",
        "auto-demo"
      ],
      "summary": "Круглую шайбу можно таскать мышью: при отпускании она продолжает лететь по инерции, трётся о «стены» и отскакивает с потерей скорости.",
      "effect": "Pointer events + setPointerCapture. Скорость считается по последнему pointermove (px/мс → px/кадр), при отпускании интегрируется в rAF: трение 0.955 за кадр, отскок от границ с restitution 0.62. Если 1.5 с нет действий, авто-демо само запускает шайбу примерно раз в 1.8 с — чтобы рекордер поймал движение.",
      "when_to_use": "Интерактивные «стикеры», примеры продукта, drag-to-dismiss карточки, игровые промо-блоки и демо физики в hero.",
      "tech": [
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"drag-stage\">\n  <div class=\"puck\" id=\"puck\">DRAG</div>\n</div>",
        "css": ".drag-stage { position: relative; height: 300px; overflow: hidden; border: 1px dashed #2c3550; border-radius: 16px; background: radial-gradient(120% 120% at 30% 15%, #161c2c 0%, #0b0d12 60%); }\n.puck { position: absolute; top: 0; left: 0; width: 84px; height: 84px; display: grid; place-items: center; border-radius: 50%; background: radial-gradient(circle at 32% 28%, #b7c1ff, #5f6cff 65%, #3d47b8); color: #0b0d12; font: 800 11px/1 ui-monospace, monospace; letter-spacing: .14em; box-shadow: 0 18px 40px rgba(95, 108, 255, .35); cursor: grab; touch-action: none; user-select: none; will-change: transform; }\n.puck:active { cursor: grabbing; }\n@media (prefers-reduced-motion: reduce) { .puck { cursor: default; } }",
        "js": "const stage = document.querySelector(\".drag-stage\");\nconst puck = document.getElementById(\"puck\");\nconst reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nlet x = 0, y = 0, vx = 0, vy = 0, drag = false;\nlet lastX = 0, lastY = 0, lastT = 0, idle = 0;\n\nfunction bounds() { return { w: stage.clientWidth - puck.offsetWidth, h: stage.clientHeight - puck.offsetHeight }; }\nfunction place() { puck.style.transform = \"translate(\" + x + \"px,\" + y + \"px)\"; }\n\nfunction physics() {\n  if (!drag) {\n    const b = bounds();\n    x += vx; y += vy; vx *= 0.955; vy *= 0.955;\n    if (x < 0) { x = 0; vx *= -0.62; }\n    if (x > b.w) { x = b.w; vx *= -0.62; }\n    if (y < 0) { y = 0; vy *= -0.62; }\n    if (y > b.h) { y = b.h; vy *= -0.62; }\n    place();\n  }\n  requestAnimationFrame(physics);\n}\n\npuck.addEventListener(\"pointerdown\", (e) => {\n  drag = true; vx = vy = 0;\n  puck.setPointerCapture(e.pointerId);\n  lastX = e.clientX; lastY = e.clientY; lastT = performance.now(); idle = lastT;\n});\npuck.addEventListener(\"pointermove\", (e) => {\n  if (!drag) return;\n  const now = performance.now();\n  const rect = stage.getBoundingClientRect();\n  x = e.clientX - rect.left - puck.offsetWidth / 2;\n  y = e.clientY - rect.top - puck.offsetHeight / 2;\n  const dt = Math.max(1, now - lastT);\n  vx = (e.clientX - lastX) / dt * 16;\n  vy = (e.clientY - lastY) / dt * 16;\n  lastX = e.clientX; lastY = e.clientY; lastT = now; idle = now;\n  place();\n});\nfunction release() { if (drag) { drag = false; idle = performance.now(); } }\npuck.addEventListener(\"pointerup\", release);\npuck.addEventListener(\"pointercancel\", release);\n\nsetInterval(() => {\n  if (reduce || drag || performance.now() - idle < 1500) return;\n  vx = (Math.random() < 0.5 ? -1 : 1) * (2.5 + Math.random() * 2);\n  vy = (Math.random() < 0.5 ? -1 : 1) * (2 + Math.random() * 2);\n  idle = performance.now();\n}, 1800);\n\nx = stage.clientWidth / 2 - 42;\ny = stage.clientHeight / 2 - 42;\nplace();\nphysics();"
      },
      "source": {
        "name": "MDN — Pointer events",
        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events",
        "domain": "developer.mozilla.org"
      },
      "performance": "Один rAF-цикл, только transform, без reflow; размеры читаются по факту события, не каждый кадр.",
      "a11y": "prefers-reduced-motion: reduce — авто-запуск отключён; ручное перетаскивание остаётся, но инерция быстро гасится.",
      "browser_support": "Chrome 55+, Safari 13+, Firefox 59+ (Pointer Events, setPointerCapture) — все актуальные.",
      "ai_use": "Use for draggable stickers/cards: pointer events with setPointerCapture; velocity = delta/ms converted to px/frame; friction 0.955 per frame and restitution 0.62 on bounds; auto-kick after 1.5s idle for demo recording.",
      "preview": {
        "scroll": false,
        "mouse": ".puck",
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-gooey-tabs",
      "name": "Gooey Sliding Tab Indicator",
      "name_ru": "Перетекающий gooey-индикатор вкладок",
      "category": "physics3d",
      "tags": [
        "svg-filter",
        "gooey",
        "tabs",
        "indicator",
        "auto-demo"
      ],
      "summary": "Активная вкладка переезжает упругим пилюльным индикатором, а позади тянется второй слой-капля: SVG-фильтр склеивает их в «тянущую» жидкую перемычку.",
      "effect": "Два одинаковых круга под табами: pill едет за активной кнопкой за .45 с cubic-bezier(.55,1.5,.4,1), blob — за .8 с, из-за разницы таймингов и SVG goo-фильтра (feGaussianBlur 7 + feColorMatrix alpha 22/-10 + feComposite) между ними натягивается капля. Авто-демо переключает вкладки раз в 1.5 с.",
      "when_to_use": "Табы категорий, переключатели тарифов, сегмент-контролы в фича-секциях; везде, где нужен живой «liquid» отклик на смену состояния.",
      "tech": [
        "css",
        "vanilla-js",
        "svg"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"goo-tabs\">\n  <svg class=\"goo-defs\" aria-hidden=\"true\" focusable=\"false\">\n    <filter id=\"goo\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\">\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"7\" result=\"blur\"/>\n      <feColorMatrix in=\"blur\" mode=\"matrix\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -10\" result=\"goo\"/>\n      <feComposite in=\"SourceGraphic\" in2=\"goo\" operator=\"atop\"/>\n    </filter>\n  </svg>\n  <div class=\"goo-layer\"><span class=\"goo-ink goo-blob\" id=\"gooBlob\"></span><span class=\"goo-ink goo-pill\" id=\"gooPill\"></span></div>\n  <nav class=\"goo-nav\" id=\"gooNav\" aria-label=\"Demo tabs\">\n    <button type=\"button\">Home</button><button type=\"button\">Work</button><button type=\"button\">Pricing</button>\n  </nav>\n</div>",
        "css": ".goo-tabs { position: relative; display: grid; place-items: center; padding: 60px 20px; font-family: system-ui, sans-serif; }\n.goo-defs { position: absolute; width: 0; height: 0; }\n.goo-layer, .goo-nav { grid-area: 1 / 1; width: 100%; }\n.goo-layer { position: relative; filter: url(#goo); }\n.goo-nav { position: relative; display: flex; justify-content: center; gap: 6px; }\n.goo-nav button { position: relative; z-index: 1; padding: 11px 20px; border: 0; border-radius: 999px; background: none; color: #aab3d0; font: 600 14px system-ui, sans-serif; cursor: pointer; transition: color .35s; }\n.goo-nav button.is-active { color: #0b0d12; }\n.goo-ink { position: absolute; top: 50%; left: 0; height: 38px; width: 0; margin-top: -19px; border-radius: 999px; background: #7c8cff; }\n.goo-pill { transition: left .45s cubic-bezier(.55, 1.5, .4, 1), width .45s cubic-bezier(.55, 1.5, .4, 1); }\n.goo-blob { background: #9aa6ff; transition: left .8s cubic-bezier(.4, 1.2, .3, 1), width .8s cubic-bezier(.4, 1.2, .3, 1); }\n@media (prefers-reduced-motion: reduce) { .goo-pill, .goo-blob { transition: none; } }",
        "js": "const nav = document.getElementById(\"gooNav\");\nconst buttons = [...nav.querySelectorAll(\"button\")];\nconst pill = document.getElementById(\"gooPill\");\nconst blob = document.getElementById(\"gooBlob\");\nconst reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nlet active = 0, timer;\n\nfunction move(i) {\n  active = i;\n  buttons.forEach((b, k) => b.classList.toggle(\"is-active\", k === i));\n  const b = buttons[i];\n  [pill, blob].forEach((el) => {\n    el.style.left = b.offsetLeft + \"px\";\n    el.style.width = b.offsetWidth + \"px\";\n  });\n}\nfunction restart() {\n  clearInterval(timer);\n  timer = setInterval(() => { if (!reduce) move((active + 1) % buttons.length); }, 1500);\n}\nbuttons.forEach((b, i) => b.addEventListener(\"click\", () => { move(i); restart(); }));\nmove(0);\nrestart();\nrequestAnimationFrame(() => move(active));"
      },
      "source": {
        "name": "CSS-Tricks — The Gooey Effect",
        "url": "https://css-tricks.com/gooey-effect/",
        "domain": "css-tricks.com"
      },
      "performance": "SVG-фильтр пересчитывается при каждом движении индикатора — умеренно дорого; анимируются только left/width одного элемента.",
      "a11y": "prefers-reduced-motion: reduce — переходы индикатора отключены, авто-переключение вкладок не запускается, клик работает мгновенно.",
      "browser_support": "SVG-фильтры и feColorMatrix: Chrome, Safari, Firefox — все актуальные версии.",
      "ai_use": "Use for pill-style tabs: two same-color shapes under an SVG goo filter (feGaussianBlur 7 + feColorMatrix alpha 22/-10), indicator moves .45s with spring, trailing blob lags at .8s to stretch the bridge; auto-rotate tabs every 1.5s in demos.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-svg-morph",
      "name": "SVG Shape Morph Between Paths",
      "name_ru": "Морфинг SVG-фигуры между path",
      "category": "physics3d",
      "tags": [
        "svg",
        "smil",
        "morph",
        "path",
        "loop"
      ],
      "summary": "Градиентная клякса непрерывно перетекает между двумя формами: одинаковое число кривых Безье интерполируется браузером прямо в атрибуте d.",
      "effect": "SMIL <animate attributeName=\"d\"> с values из двух путей (M + 4×C + Z в каждом — структура обязана совпадать), dur 2.4 с, repeatCount indefinite, calcMode spline с 0.45 0 0.55 1: асимметричный блоб плавно превращается в круглый и обратно. При reduced-motion svg.pauseAnimations() замораживает первый кадр.",
      "when_to_use": "Акценты в hero (логотип-клякса), декор секций, иконки-иллюстрации состояния «загрузка/готово», буллеты преимуществ.",
      "tech": [
        "svg",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"morph-demo\">\n  <svg viewBox=\"0 0 120 120\" width=\"200\" height=\"200\" id=\"morphSvg\" role=\"img\" aria-label=\"Morphing blob\">\n    <defs>\n      <linearGradient id=\"morphGrad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n        <stop offset=\"0\" stop-color=\"#7c8cff\"/><stop offset=\"1\" stop-color=\"#ff7a59\"/>\n      </linearGradient>\n    </defs>\n    <path fill=\"url(#morphGrad)\" d=\"M60,12 C88,12 108,28 108,52 C108,78 86,96 60,96 C34,96 12,78 12,52 C12,28 32,12 60,12 Z\">\n      <animate id=\"morphAnim\" attributeName=\"d\" dur=\"2.4s\" repeatCount=\"indefinite\" calcMode=\"spline\" keyTimes=\"0;0.5;1\" keySplines=\"0.45 0 0.55 1;0.45 0 0.55 1\"\n        values=\"M60,12 C88,12 108,28 108,52 C108,78 86,96 60,96 C34,96 12,78 12,52 C12,28 32,12 60,12 Z;M60,10 C92,4 112,24 110,48 C108,72 96,92 72,98 C48,104 18,88 12,62 C6,36 30,16 60,10 Z;M60,12 C88,12 108,28 108,52 C108,78 86,96 60,96 C34,96 12,78 12,52 C12,28 32,12 60,12 Z\"/>\n    </path>\n  </svg>\n  <p class=\"morph-cap\">SVG path morph · 2.4s loop</p>\n</div>",
        "css": ".morph-demo { display: grid; place-items: center; gap: 10px; padding: 22px; font-family: system-ui, sans-serif; }\n.morph-cap { margin: 0; font: 600 10px/1 ui-monospace, monospace; letter-spacing: .16em; text-transform: uppercase; color: #6f7796; }\n@media (prefers-reduced-motion: reduce) { .morph-cap::after { content: \" · static\"; } }",
        "js": "if (matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n  document.getElementById(\"morphSvg\").pauseAnimations();\n}"
      },
      "source": {
        "name": "MDN — <animate> (SVG)",
        "url": "https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/animate",
        "domain": "developer.mozilla.org"
      },
      "performance": "Пересчёт одного пути на кадр — дёшево; не запускать десятками одновременно (каждый путь перерисовывает слой).",
      "a11y": "prefers-reduced-motion: reduce — svg.pauseAnimations() останавливает SMIL, фигура замирает на первом кадре.",
      "browser_support": "SMIL-анимации и интерполяция d: Chrome, Safari, Firefox (все актуальные); пути обязаны совпадать по структуре команд.",
      "ai_use": "Use for blob/logo accents: SMIL <animate attributeName=\"d\"> looping two paths that share an identical M/C/Z command structure (4 cubic segments), dur 2.4s spline; call svg.pauseAnimations() under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-flip-card",
      "name": "CSS 3D Flip Card",
      "name_ru": "3D-переворот карточки",
      "category": "physics3d",
      "tags": [
        "3d",
        "rotatey",
        "flip",
        "hover",
        "preserve-3d",
        "auto-demo"
      ],
      "summary": "Карточка тарифа физически переворачивается вокруг вертикальной оси на 180°: лицевая сторона уходит, обратная с ценой приходит из глубины.",
      "effect": "Обёртка с perspective: 900px, у карточки transform-style: preserve-3d и transition transform .9s cubic-bezier(.3,1.3,.4,1) — лёгкий overshoot в конце. Срабатывает на hover и авто-демо (класс is-flipped каждые 2.4 с), чтобы рекордер видел движение. Стороны скрыты backface-visibility: hidden.",
      "when_to_use": "Тарифы и прайсинг, карточки товаров, флипы «вопрос→ответ», промо-плитки с ценой на обороте.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<div class=\"flip-scene\">\n  <div class=\"flip-card\" id=\"flipCard\">\n    <div class=\"flip-face flip-front\">\n      <span class=\"flip-badge\">PRO</span>\n      <h3>Analytics</h3>\n      <p>Realtime metrics for your team</p>\n    </div>\n    <div class=\"flip-face flip-back\">\n      <h3>$12 / mo</h3>\n      <p>Billed yearly · cancel anytime</p>\n      <button type=\"button\">Choose Pro</button>\n    </div>\n  </div>\n</div>",
        "css": ".flip-scene { display: grid; place-items: center; padding: 56px 20px; perspective: 900px; font-family: system-ui, sans-serif; }\n.flip-card { position: relative; width: 250px; height: 156px; transform-style: preserve-3d; transition: transform .9s cubic-bezier(.3, 1.3, .4, 1); }\n.flip-scene:hover .flip-card, .flip-card.is-flipped { transform: rotateY(180deg); }\n.flip-face { position: absolute; inset: 0; display: grid; align-content: center; gap: 6px; padding: 18px; border-radius: 16px; backface-visibility: hidden; -webkit-backface-visibility: hidden; }\n.flip-front { background: linear-gradient(140deg, #1c2337, #141827); border: 1px solid #2c3550; color: #eef1ff; }\n.flip-front h3 { margin: 0; font-size: 21px; }\n.flip-front p, .flip-back p { margin: 0; font-size: 13px; }\n.flip-front p { color: #98a2c4; }\n.flip-badge { justify-self: start; padding: 3px 9px; border-radius: 999px; background: #7c8cff; color: #0b0d12; font: 700 10px/1 ui-monospace, monospace; letter-spacing: .12em; }\n.flip-back { background: linear-gradient(140deg, #7c8cff, #a45cff); color: #0b0d12; transform: rotateY(180deg); }\n.flip-back h3 { margin: 0; font-size: 24px; }\n.flip-back p { opacity: .8; }\n.flip-back button { justify-self: start; margin-top: 8px; padding: 8px 14px; border: 0; border-radius: 10px; background: #0b0d12; color: #eef1ff; font-weight: 600; cursor: pointer; }\n@media (prefers-reduced-motion: reduce) { .flip-card { transition: none; } }",
        "js": "const flipCard = document.getElementById(\"flipCard\");\nif (!matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n  setInterval(() => flipCard.classList.toggle(\"is-flipped\"), 2400);\n}"
      },
      "source": {
        "name": "Intro to CSS 3D transforms — Card Flip (David DeSandro)",
        "url": "https://3dtransforms.desandro.com/card-flip",
        "domain": "3dtransforms.desandro.com"
      },
      "performance": "rotateY и backface-visibility — compositor-only, без layout; дешево даже для сетки карточек.",
      "a11y": "prefers-reduced-motion: reduce — transition: none, авто-переворот не запускается; hover всё ещё меняет сторону мгновенно.",
      "browser_support": "3D-трансформы: Chrome 36+, Safari 9+, Firefox 16+; актуальные версии — все.",
      "ai_use": "Use for pricing/feature cards: perspective 900px wrapper, preserve-3d faces, rotateY(180deg) toggle, transition .9s cubic-bezier(.3,1.3,.4,1), backface-visibility hidden; auto-flip every 2.4s in demos.",
      "preview": {
        "scroll": false,
        "mouse": ".flip-scene",
        "wait_ms": 1600,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-tilt-gallery",
      "name": "3D Perspective Tilt on Mousemove",
      "name_ru": "3D-наклон за курсором",
      "category": "physics3d",
      "tags": [
        "3d",
        "tilt",
        "mousemove",
        "parallax",
        "preserve-3d",
        "auto-demo"
      ],
      "summary": "Карточка наклоняется вслед за курсором, а внутренние слои (заголовок, чип, орб) расходятся по глубине — эффект тонкой 3D-сцены.",
      "effect": "pointermove задаёт цель rotateX/rotateY (±22–26°), значения сглаживаются lerp 0.12 в rAF — движение мягкое, с инерцией. Дети с translateZ 18–70px дают parallax. Если 0.9 с нет курсора, авто-демо ведёт цель по синусоидам, чтобы анимацию поймал рекордер.",
      "when_to_use": "Портфолио и кейсы, галереи фич, продуктовые карточки в hero, интерактивные превью интерфейсов.",
      "tech": [
        "vanilla-js",
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"tilt-stage\" id=\"tiltStage\">\n  <div class=\"tilt-card\">\n    <span class=\"tilt-chip\">NEW</span>\n    <h3>Vertex Studio</h3>\n    <p>Interactive product tours</p>\n    <span class=\"tilt-orb\"></span>\n  </div>\n  <p class=\"tilt-hint\">Move cursor · 3D tilt</p>\n</div>",
        "css": ".tilt-stage { display: grid; place-items: center; padding: 34px 20px 0; perspective: 900px; font-family: system-ui, sans-serif; }\n.tilt-card { position: relative; width: 250px; height: 168px; padding: 20px; border-radius: 18px; background: linear-gradient(150deg, #1b2233, #12161f); border: 1px solid #2c3550; color: #eef1ff; transform-style: preserve-3d; will-change: transform; box-shadow: 0 24px 60px rgba(0, 0, 0, .45); }\n.tilt-card h3 { margin: 0 0 4px; font-size: 22px; transform: translateZ(34px); }\n.tilt-card p { margin: 0; color: #98a2c4; font-size: 13px; transform: translateZ(18px); }\n.tilt-chip { position: absolute; top: 14px; right: 14px; padding: 4px 10px; border-radius: 999px; background: #ff7a59; color: #0b0d12; font: 700 10px/1 ui-monospace, monospace; transform: translateZ(52px); }\n.tilt-orb { position: absolute; right: 22px; bottom: 18px; width: 34px; height: 34px; border-radius: 50%; background: radial-gradient(circle at 32% 30%, #b7c1ff, #5f6cff); transform: translateZ(70px); }\n.tilt-hint { margin: 24px 0 0; font: 600 10px/1 ui-monospace, monospace; letter-spacing: .16em; text-transform: uppercase; color: #6f7796; }\n@media (prefers-reduced-motion: reduce) { .tilt-card { box-shadow: 0 24px 60px rgba(0, 0, 0, .45); } }",
        "js": "const stage = document.getElementById(\"tiltStage\");\nconst card = stage.querySelector(\".tilt-card\");\nconst reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nlet tx = 0.5, ty = 0.5, cx = 0.5, cy = 0.5, lastMove = 0;\nconst t0 = performance.now();\n\nstage.addEventListener(\"pointermove\", (e) => {\n  const rect = stage.getBoundingClientRect();\n  tx = (e.clientX - rect.left) / rect.width;\n  ty = (e.clientY - rect.top) / rect.height;\n  lastMove = performance.now();\n});\n\nif (reduce) {\n  card.style.transform = \"rotateX(6deg) rotateY(-8deg)\";\n} else {\n  (function loop(now) {\n    if (now - lastMove > 900) {\n      tx = 0.5 + 0.34 * Math.sin((now - t0) / 900);\n      ty = 0.5 + 0.30 * Math.sin((now - t0) / 640);\n    }\n    cx += (tx - cx) * 0.12;\n    cy += (ty - cy) * 0.12;\n    card.style.transform = \"rotateX(\" + (0.5 - cy) * 22 + \"deg) rotateY(\" + (cx - 0.5) * 26 + \"deg)\";\n    requestAnimationFrame(loop);\n  })(performance.now());\n}"
      },
      "source": {
        "name": "MDN — transform-style (preserve-3d)",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/transform-style",
        "domain": "developer.mozilla.org"
      },
      "performance": "Transform-only в rAF, will-change: transform; getBoundingClientRect — раз на pointermove, не каждый кадр.",
      "a11y": "prefers-reduced-motion: reduce — rAF-цикл не запускается, карточка статична с фиксированным наклоном rotateX(6deg) rotateY(-8deg).",
      "browser_support": "preserve-3d и perspective: Chrome 36+, Safari 9+, Firefox 16+ — все актуальные.",
      "ai_use": "Use for gallery/product cards: pointermove maps cursor position to rotateX/rotateY (max ±22-26deg) lerped at 0.12 in rAF; inner layers use translateZ 18-70px; add an idle sine auto-demo for recordings.",
      "preview": {
        "scroll": false,
        "mouse": ".tilt-stage",
        "wait_ms": 1800,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-rolling-cube",
      "name": "Rolling 3D Cube",
      "name_ru": "Катящийся 3D-куб",
      "category": "physics3d",
      "tags": [
        "3d",
        "cube",
        "keyframes",
        "loop",
        "transform-style"
      ],
      "summary": "Полупрозрачный куб из шести граней ездит туда-сюда, одновременно вращаясь на −360° по Y, и отбрасывает пульсирующую тень.",
      "effect": "6 граней с translateZ(52px) и поворотами по осям; анимация cube-roll 3.6 с alternate infinite: translateX(−84px → 84px) с rotateX(−16deg) и rotateY(0 → −360deg), easing cubic-bezier(.55,.1,.45,.9). Тень-эллипс синхронизирована вторыми keyframes и меняет масштаб/прозрачность.",
      "when_to_use": "Игровые и продуктовые hero-блоки, промо 3D-редакторов, декор секции «как это работает», паузы между блоками.",
      "tech": [
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"cube-scene\">\n  <div class=\"cube\">\n    <i class=\"cf cf-front\"></i><i class=\"cf cf-back\"></i>\n    <i class=\"cf cf-right\"></i><i class=\"cf cf-left\"></i>\n    <i class=\"cf cf-top\"></i><i class=\"cf cf-bottom\"></i>\n  </div>\n  <span class=\"cube-shadow\"></span>\n</div>",
        "css": ".cube-scene { position: relative; display: grid; place-items: center; padding: 78px 0 42px; perspective: 760px; font-family: system-ui, sans-serif; }\n.cube { position: relative; width: 104px; height: 104px; transform-style: preserve-3d; animation: cube-roll 3.6s cubic-bezier(.55, .1, .45, .9) infinite alternate; }\n.cf { position: absolute; inset: 0; border-radius: 14px; border: 1px solid rgba(255, 255, 255, .28); background: linear-gradient(140deg, rgba(124, 140, 255, .92), rgba(124, 140, 255, .32)); box-shadow: inset 0 0 24px rgba(255, 255, 255, .18); }\n.cf-front { transform: translateZ(52px); }\n.cf-back { transform: rotateY(180deg) translateZ(52px); }\n.cf-right { transform: rotateY(90deg) translateZ(52px); }\n.cf-left { transform: rotateY(-90deg) translateZ(52px); }\n.cf-top { transform: rotateX(90deg) translateZ(52px); }\n.cf-bottom { transform: rotateX(-90deg) translateZ(52px); }\n.cube-shadow { position: absolute; bottom: 26px; width: 132px; height: 16px; border-radius: 50%; background: radial-gradient(closest-side, rgba(124, 140, 255, .45), transparent); animation: cube-shadow 3.6s cubic-bezier(.55, .1, .45, .9) infinite alternate; }\n@keyframes cube-roll { from { transform: translateX(-84px) rotateX(-16deg) rotateY(0deg); } to { transform: translateX(84px) rotateX(-16deg) rotateY(-360deg); } }\n@keyframes cube-shadow { from { transform: translateX(-84px) scale(1); opacity: 1; } to { transform: translateX(84px) scale(1.15); opacity: .55; } }\n@media (prefers-reduced-motion: reduce) {\n  .cube, .cube-shadow { animation: none; }\n  .cube { transform: rotateX(-18deg) rotateY(-34deg); }\n}",
        "js": ""
      },
      "source": {
        "name": "Intro to CSS 3D transforms — Cube (David DeSandro)",
        "url": "https://3dtransforms.desandro.com/cube",
        "domain": "3dtransforms.desandro.com"
      },
      "performance": "6 элементов, только transform — compositor-only, infinite-анимация почти бесплатна; use will-change на кубе при множестве кубов.",
      "a11y": "prefers-reduced-motion: reduce — animation: none, куб показан статично в наклонной позе, тень фиксирована.",
      "browser_support": "3D-трансформы и preserve-3d: Chrome 36+, Safari 9+, Firefox 16+ — все актуальные.",
      "ai_use": "Use as a playful hero prop: 6-face CSS cube, faces at translateZ(52px), 3.6s alternate infinite keyframes rolling translateX(-84px to 84px) with rotateY 0 to -360deg plus a synced shadow ellipse.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-three-torus",
      "name": "Three.js Rotating Object Scene",
      "name_ru": "Three.js: вращающийся тор-узел",
      "category": "physics3d",
      "tags": [
        "webgl",
        "three",
        "3d",
        "metal",
        "loop",
        "esm-cdn"
      ],
      "summary": "Металлический тор-узел вращается в WebGL-сцене с двумя источниками света — минимальная, но полноценная 3D-сцена в 25 строк.",
      "effect": "three.js подключается динамическим import() (классический скрипт, без module-тегов). TorusKnotGeometry 180×26 + MeshStandardMaterial (metalness .65, roughness .22), HemisphereLight + DirectionalLight. Вращение в renderer.setAnimationLoop: rotateY 0.0006 рад/мс, rotateX — синус ±0.4. При reduced-motion кадр статичен; resize отслеживается.",
      "when_to_use": "Hero-сцена продукта, 3D-иконка фичи, фон CTA, демонстрация «нашего 3D» без тяжёлых моделей и загрузок ассетов.",
      "tech": [
        "three",
        "webgl",
        "vanilla-js"
      ],
      "dependencies": [
        "three 0.160 (ES module, jsDelivr CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"three-wrap\" id=\"threeWrap\"></div>",
        "css": ".three-wrap { width: 100%; height: 300px; background: radial-gradient(120% 120% at 70% 20%, #141a2b 0%, #0b0d12 60%); }\n.three-wrap canvas { display: block; }",
        "js": "(async () => {\n  const THREE = await import(\"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js\");\n  const wrap = document.getElementById(\"threeWrap\");\n  const reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));\n  renderer.setSize(wrap.clientWidth, wrap.clientHeight);\n  wrap.appendChild(renderer.domElement);\n\n  const scene = new THREE.Scene();\n  const camera = new THREE.PerspectiveCamera(42, wrap.clientWidth / wrap.clientHeight, 0.1, 50);\n  camera.position.set(0, 0.4, 5.4);\n\n  const knot = new THREE.Mesh(\n    new THREE.TorusKnotGeometry(1.15, 0.36, 180, 26),\n    new THREE.MeshStandardMaterial({ color: 0x8f9dff, roughness: 0.22, metalness: 0.65 })\n  );\n  scene.add(knot);\n  scene.add(new THREE.HemisphereLight(0xbfd0ff, 0x0b0d12, 1.2));\n  const key = new THREE.DirectionalLight(0xffffff, 2.4);\n  key.position.set(3, 4, 5);\n  scene.add(key);\n\n  addEventListener(\"resize\", () => {\n    renderer.setSize(wrap.clientWidth, wrap.clientHeight);\n    camera.aspect = wrap.clientWidth / wrap.clientHeight;\n    camera.updateProjectionMatrix();\n  });\n\n  renderer.setAnimationLoop((t) => {\n    if (!reduce) {\n      knot.rotation.y = t * 0.0006;\n      knot.rotation.x = Math.sin(t * 0.0004) * 0.4;\n    }\n    renderer.render(scene, camera);\n  });\n})();"
      },
      "source": {
        "name": "three.js docs — Installation (import from CDN)",
        "url": "https://threejs.org/docs/index.html#manual/en/introduction/Installation",
        "domain": "threejs.org"
      },
      "performance": "Один draw call, геометрия создаётся один раз; держать 1–2 WebGL-сцены на страницу, ставить на паузу вне вьюпорта.",
      "a11y": "prefers-reduced-motion: reduce — вращение отключено, рендерится статичный кадр (цикл нужен только для resize).",
      "browser_support": "WebGL2: Chrome 56+, Safari 15+, Firefox 51+; динамический import() — все актуальные браузеры.",
      "ai_use": "Use for hero/product 3D: dynamically import three@0.160 module from jsDelivr, torus knot with hemisphere + directional light, rotate y += 0.0006 rad/ms in setAnimationLoop; skip rotation under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-r3f-scene",
      "name": "React Three Fiber Minimal Scene",
      "name_ru": "Мини-сцена на React Three Fiber",
      "category": "physics3d",
      "tags": [
        "webgl",
        "react",
        "three",
        "r3f",
        "esm-cdn",
        "loop"
      ],
      "summary": "Тот же 3D-приём, но декларативно: React 18 + @react-three/fiber прямо с esm.sh, без сборщика — плоский icosahedron крутится через useFrame.",
      "effect": "Один файл: динамические import() с esm.sh (react@18.2, react-dom/client, @react-three/fiber@8.15 с зафиксированными deps на three@0.160). Canvas + useFrame вращает mesh: rotation.y += delta*0.9, rotation.x += delta*0.35; flatShading-материал цвета коралл, ambientLight + directionalLight. При reduced-motion вращение не применяется.",
      "when_to_use": "Лендинги на React/Next, где 3D-сцена должна жить внутри компонента рядом с обычной разметкой; прототипы R3F без сборки.",
      "tech": [
        "react",
        "three",
        "webgl"
      ],
      "dependencies": [
        "react 18.2 + react-dom 18.2 + @react-three/fiber 8.15.19 + three 0.160 (esm.sh, ?deps-пиннинг)"
      ],
      "difficulty": "hard",
      "code": {
        "html": "<div class=\"r3f-demo\" id=\"r3fRoot\"></div>",
        "css": ".r3f-demo { width: 100%; height: 300px; background: radial-gradient(120% 120% at 30% 10%, #1a1420 0%, #0b0d12 65%); }\n.r3f-demo canvas { display: block; }",
        "js": "(async () => {\n  const [{ default: React }, { createRoot }, fiber] = await Promise.all([\n    import(\"https://esm.sh/react@18.2.0\"),\n    import(\"https://esm.sh/react-dom@18.2.0/client?deps=react@18.2.0\"),\n    import(\"https://esm.sh/@react-three/fiber@8.15.19?deps=react@18.2.0,react-dom@18.2.0,three@0.160.0\"),\n  ]);\n  const { Canvas, useFrame } = fiber;\n  const h = React.createElement;\n  const reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n  function Spinner() {\n    const ref = React.useRef();\n    useFrame((state, delta) => {\n      if (!ref.current || reduce) return;\n      ref.current.rotation.y += delta * 0.9;\n      ref.current.rotation.x += delta * 0.35;\n    });\n    return h(\"mesh\", { ref },\n      h(\"icosahedronGeometry\", { args: [1.25, 1] }),\n      h(\"meshStandardMaterial\", { color: \"#ff7a59\", roughness: 0.28, metalness: 0.5, flatShading: true })\n    );\n  }\n\n  createRoot(document.getElementById(\"r3fRoot\")).render(\n    h(Canvas, { dpr: [1, 2], camera: { position: [0, 0, 4.4] } },\n      h(\"ambientLight\", { intensity: 0.7 }),\n      h(\"directionalLight\", { position: [3, 4, 5], intensity: 2.2 }),\n      h(Spinner)\n    )\n  );\n})();"
      },
      "source": {
        "name": "React Three Fiber — Introduction",
        "url": "https://r3f.docs.pmnd.rs/getting-started/introduction",
        "domain": "r3f.docs.pmnd.rs"
      },
      "performance": "React 18 + three над R3F — заметный JS-вес (~130 КБ gzip); один canvas на страницу, не запускать много сцен.",
      "a11y": "prefers-reduced-motion: reduce — useFrame не меняет rotation, сцена рендерится статичным кадром.",
      "browser_support": "Требуются ES modules + import maps-совместимые динамические импорты: Chrome 63+, Safari 11.1+, Firefox 67+; React 18.",
      "ai_use": "Use when the landing is React-based: load React 18.2 + @react-three/fiber 8.15 from esm.sh with ?deps pinning three@0.160, mount Canvas, rotate a flat-shaded icosahedron in useFrame (delta-based), disable under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 3500,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-lottie-vector-loop",
      "name": "Lottie Vector Loop (Inline JSON)",
      "name_ru": "Lottie-анимация на inline JSON",
      "category": "physics3d",
      "tags": [
        "lottie",
        "vector",
        "loop",
        "inline-json",
        "after-effects"
      ],
      "summary": "Пульсирующий круг и вращающаяся точка-орбита из Lottie-файла, вшитого прямо в код — ноль сетевых запросов за JSON, только плеер с CDN.",
      "effect": "lottie-web с animationData-объектом: 60 fps, 120 кадров (2 с), две shape-слоя — круг масштабируется 58%→104%→58% и точка обегает орбиту радиусом 86 px за полный оборот 360°. loop: true; при reduced-motion autoplay выключен, goToAndStop(30) показывает статичный кадр.",
      "when_to_use": "Иконки-состояния, анимация успеха/загрузки, лёгкий декор в кнопках и пустых состояниях; когда дизайнер отдал After Effects-анимацию.",
      "tech": [
        "lottie",
        "vanilla-js"
      ],
      "dependencies": [
        "lottie-web 5.12.2 (jsDelivr CDN)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/lottie-web@5.12.2/build/player/lottie.min.js\"></script>\n<div class=\"lottie-demo\">\n  <div class=\"lottie-box\" id=\"lottieBox\"></div>\n  <p class=\"lottie-cap\">Lottie · vector loop 2s</p>\n</div>",
        "css": ".lottie-demo { display: grid; place-items: center; gap: 8px; padding: 20px; font-family: system-ui, sans-serif; }\n.lottie-box { width: 190px; height: 190px; }\n.lottie-cap { margin: 0; font: 600 10px/1 ui-monospace, monospace; letter-spacing: .16em; text-transform: uppercase; color: #6f7796; }\n@media (prefers-reduced-motion: reduce) { .lottie-cap::after { content: \" · static\"; } }",
        "js": "const reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nconst animData = {\n  v: \"5.12.2\", fr: 60, ip: 0, op: 120, w: 240, h: 240, nm: \"pulse-orbit\", ddd: 0,\n  assets: [],\n  layers: [\n    { ddd: 0, ind: 1, ty: 4, nm: \"pulse\", sr: 1, ao: 0,\n      ks: { o: { a: 0, k: 100 }, r: { a: 0, k: 0 }, p: { a: 0, k: [120, 120, 0] }, a: { a: 0, k: [0, 0, 0] },\n        s: { a: 1, k: [\n          { i: { x: [0.45, 0.45, 0.45], y: [1, 1, 1] }, o: { x: [0.55, 0.55, 0.55], y: [0, 0, 0] }, t: 0, s: [58, 58, 100] },\n          { i: { x: [0.45, 0.45, 0.45], y: [1, 1, 1] }, o: { x: [0.55, 0.55, 0.55], y: [0, 0, 0] }, t: 60, s: [104, 104, 100] },\n          { t: 120, s: [58, 58, 100] }] } },\n      shapes: [{ ty: \"gr\", nm: \"pulse-grp\", it: [\n        { ty: \"el\", d: 1, p: { a: 0, k: [0, 0] }, s: { a: 0, k: [150, 150] }, nm: \"ellipse\" },\n        { ty: \"fl\", c: { a: 0, k: [0.486, 0.549, 1, 1] }, o: { a: 0, k: 100 }, r: 1, nm: \"fill\" },\n        { ty: \"tr\", p: { a: 0, k: [0, 0] }, a: { a: 0, k: [0, 0] }, s: { a: 0, k: [100, 100] }, r: { a: 0, k: 0 }, o: { a: 0, k: 100 } }] }],\n      ip: 0, op: 120, st: 0, bm: 0 },\n    { ddd: 0, ind: 2, ty: 4, nm: \"orbit\", sr: 1, ao: 0,\n      ks: { o: { a: 0, k: 100 },\n        r: { a: 1, k: [\n          { i: { x: [0.35], y: [1] }, o: { x: [0.65], y: [0] }, t: 0, s: [0] },\n          { t: 120, s: [360] }] },\n        p: { a: 0, k: [120, 120, 0] }, a: { a: 0, k: [0, 0, 0] }, s: { a: 0, k: [100, 100, 100] } },\n      shapes: [{ ty: \"gr\", nm: \"orbit-grp\", it: [\n        { ty: \"el\", d: 1, p: { a: 0, k: [0, -86] }, s: { a: 0, k: [30, 30] }, nm: \"dot\" },\n        { ty: \"fl\", c: { a: 0, k: [1, 0.478, 0.349, 1] }, o: { a: 0, k: 100 }, r: 1, nm: \"fill\" },\n        { ty: \"tr\", p: { a: 0, k: [0, 0] }, a: { a: 0, k: [0, 0] }, s: { a: 0, k: [100, 100] }, r: { a: 0, k: 0 }, o: { a: 0, k: 100 } }] }],\n      ip: 0, op: 120, st: 0, bm: 0 }\n  ]\n};\nconst anim = lottie.loadAnimation({\n  container: document.getElementById(\"lottieBox\"),\n  renderer: \"svg\", loop: true, autoplay: !reduce, animationData: animData\n});\nif (reduce) anim.goToAndStop(30, true);"
      },
      "source": {
        "name": "airbnb/lottie-web — GitHub (usage & animationData)",
        "url": "https://github.com/airbnb/lottie-web",
        "domain": "github.com"
      },
      "performance": "SVG-рендерер Lottie перерисовывает сцену каждый кадр; для небольших сцен дёшево, для сложных AE-сцен — canvas-рендерер и ленивая инициализация.",
      "a11y": "prefers-reduced-motion: reduce — autoplay: false, goToAndStop(30, true) замораживает кадр посередине цикла.",
      "browser_support": "lottie-web 5.12 — Chrome/Edge/Safari/Firefox актуальные; SVG-рендерер работает без WebGL.",
      "ai_use": "Use for icon/empty-state motion: lottie-web from jsDelivr with inline animationData (no JSON fetch), 60fps 2s loop; disable autoplay and seek to a mid frame under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 1500,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-rive-embed",
      "name": "Rive State Machine Embed",
      "name_ru": "Rive-анимация (state machine) на canvas",
      "category": "physics3d",
      "tags": [
        "rive",
        "canvas",
        "state-machine",
        "wasm",
        "embed",
        "loop"
      ],
      "summary": "Официальный .riv-файл Rive играет state machine «bumpy» на canvas через рантайм с CDN — интерактивная векторная анимация без ручного кода.",
      "effect": "Скрипт @rive-app/canvas-single (WASM внутри одного JS) даёт глобальный rive; new rive.Rive({src: vehicles.riv, stateMachine: \"bumpy\", autoplay: true}) рисует бесконечный цикл машинки. resizeDrawingSurfaceToCanvas() в onLoad синхронизирует плотность пикселей; при reduced-motion autoplay: false и r.pause() после загрузки. Рантайм и .riv тянутся по сети — на медленном канале до 2–5 с, поэтому запасной фон и подпись видны сразу.",
      "when_to_use": "Интерактивные маскоты и иллюстрации, микро-игры, анимированные состояния загрузки/успеха, hero-иллюстрация с реакцией на действия пользователя.",
      "tech": [
        "rive",
        "canvas"
      ],
      "dependencies": [
        "@rive-app/canvas-single 2.21.6 (jsDelivr) + внешний .riv (cdn.rive.app)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/@rive-app/canvas-single@2.21.6/rive.min.js\"></script>\n<div class=\"rive-demo\">\n  <canvas id=\"riveCanvas\" width=\"480\" height=\"300\"></canvas>\n  <span class=\"rive-cap\">Rive · state machine «bumpy»</span>\n</div>",
        "css": ".rive-demo { position: relative; width: 100%; height: 300px; background: radial-gradient(110% 110% at 50% 0%, #1a2133 0%, #0b0d12 70%); }\n#riveCanvas { display: block; width: 100%; height: 100%; }\n.rive-cap { position: absolute; left: 14px; bottom: 10px; padding: 5px 9px; border-radius: 999px; background: rgba(11, 13, 18, .72); font: 600 10px/1 ui-monospace, monospace; letter-spacing: .16em; text-transform: uppercase; color: #8b93b0; }\n@media (prefers-reduced-motion: reduce) { .rive-cap::after { content: \" · paused\"; } }",
        "js": "const reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nconst r = new rive.Rive({\n  src: \"https://cdn.rive.app/animations/vehicles.riv\",\n  canvas: document.getElementById(\"riveCanvas\"),\n  stateMachine: \"bumpy\",\n  autoplay: !reduce,\n  onLoad: () => {\n    r.resizeDrawingSurfaceToCanvas();\n    if (reduce) r.pause();\n  },\n});\naddEventListener(\"resize\", () => r.resizeDrawingSurfaceToCanvas());"
      },
      "source": {
        "name": "Rive docs — Web (JS) runtime: Getting started",
        "url": "https://help.rive.app/runtimes/overview/web-js",
        "domain": "help.rive.app"
      },
      "performance": "Один canvas; canvas-single кладёт WASM внутрь JS (один запрос ~1.7 МБ) — это тяжелее обычной сборки, зато нет второй загрузки wasm. Держать 1–3 инстанса, не грузить все сразу.",
      "a11y": "prefers-reduced-motion: reduce — autoplay: false и r.pause() после onLoad: состояние статично, но canvas остаётся видимым.",
      "browser_support": "Canvas2D + WASM: Chrome 57+, Safari 11+, Firefox 52+; рантайм @rive-app/canvas-single 2.21 проверен в актуальных (jsDelivr-сборка запускается и в headless-Chrome).",
      "ai_use": "Use for interactive brand mascots: load @rive-app/canvas from unpkg, instantiate rive.Rive with the .riv src and its state machine name, call resizeDrawingSurfaceToCanvas() in onLoad; pause under prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 5000,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-gsap-flip",
      "name": "GSAP Flip: Card to Modal",
      "name_ru": "GSAP Flip: из карточки в модалку",
      "category": "physics3d",
      "tags": [
        "gsap",
        "flip",
        "shared-element",
        "modal",
        "auto-demo"
      ],
      "summary": "Маленькая карточка кейса перетекает в модальное окно: GSAP Flip запоминает позицию до и после смены класса и превращает разницу в плавный перелёт.",
      "effect": "Flip.getState(card) → class is-modal (position: fixed, 300×196, по центру) → Flip.from(state, {duration .75, ease power3.inOut, absolute: true}). Авто-демо: открытие через 0.9 с, закрытие через 1.7 с, цикл бесконечный; клик по карточке переключает вручную. При reduced-motion duration 0.001 — мгновенная смена.",
      "when_to_use": "Портфолио-кейсы, галерея → детальный просмотр, карточка товара → быстрый просмотр, Todo → редактирование без перезагрузки.",
      "tech": [
        "gsap",
        "vanilla-js"
      ],
      "dependencies": [
        "gsap 3.13 + Flip (jsDelivr CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Flip.min.js\"></script>\n<div class=\"gdemo\">\n  <article class=\"mini-card\" id=\"miniCard\">\n    <span class=\"mini-tag\">Case study</span>\n    <h3>Checkout 2.0</h3>\n    <p class=\"mini-extra\">Rebuilt the funnel: +23% conversion in six weeks.</p>\n    <button type=\"button\" class=\"mini-btn\">Open case</button>\n  </article>\n</div>",
        "css": ".gdemo { position: relative; min-height: 300px; display: grid; place-items: start center; padding-top: 56px; font-family: system-ui, sans-serif; }\n.mini-card { box-sizing: border-box; width: 210px; padding: 16px; border-radius: 14px; background: #161a26; border: 1px solid #2a3145; color: #eef1ff; box-shadow: 0 10px 30px rgba(0, 0, 0, .35); cursor: pointer; }\n.mini-card h3 { margin: 8px 0 4px; font-size: 19px; }\n.mini-card p { margin: 0 0 12px; color: #9aa3c0; font-size: 12.5px; }\n.mini-tag { font: 700 10px/1 ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: #8ea0ff; }\n.mini-btn { padding: 8px 13px; border: 0; border-radius: 10px; background: #7c8cff; color: #0b0d12; font-weight: 700; font-size: 12.5px; cursor: pointer; }\n.mini-extra { display: none; }\n.mini-card.is-modal { position: fixed; inset: 0; margin: auto; width: 322px; height: 224px; padding: 24px; border-radius: 20px; background: #1b2030; border-color: #3d4a6d; box-shadow: 0 30px 90px rgba(0, 0, 0, .55); z-index: 5; }\n.mini-card.is-modal .mini-extra { display: block; }\n@media (prefers-reduced-motion: reduce) { .mini-card { cursor: default; } }",
        "js": "const card = document.getElementById(\"miniCard\");\nconst reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nlet open = false, busy = false;\n\nfunction toggle() {\n  if (busy) return;\n  busy = true;\n  const state = Flip.getState(card);\n  open = !open;\n  card.classList.toggle(\"is-modal\", open);\n  Flip.from(state, {\n    duration: reduce ? 0.001 : 0.75,\n    ease: \"power3.inOut\",\n    absolute: true,\n    onComplete: () => {\n      busy = false;\n      setTimeout(toggle, open ? 1700 : 1200);\n    },\n  });\n}\ncard.addEventListener(\"click\", toggle);\nsetTimeout(toggle, 900);"
      },
      "source": {
        "name": "GSAP docs — Flip plugin",
        "url": "https://gsap.com/docs/v3/Plugins/Flip/",
        "domain": "gsap.com"
      },
      "performance": "Flip на время перехода включает position/размеры (layout), поэтому применять к одной карточке по действию, не к списку на скролле.",
      "a11y": "prefers-reduced-motion: reduce — Flip.from с duration 0.001: состояние меняется мгновенно, без перелёта.",
      "browser_support": "GSAP 3.13 + Flip: Chrome 60+, Safari 11+, Firefox 55+ — все актуальные.",
      "ai_use": "Use for shared-element card-to-modal transitions: Flip.getState before toggling the modal class, Flip.from(state, {duration:.75, ease:'power3.inOut', absolute:true}); auto-toggle open/close on a timer for demos and set duration ~0 under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 3200,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "phys-magnetic-trail",
      "name": "Magnetic Image Reveal Trail",
      "name_ru": "Магнитный шлейф изображений за курсором",
      "category": "physics3d",
      "tags": [
        "cursor",
        "trail",
        "magnetic",
        "spring",
        "auto-demo",
        "portfolio"
      ],
      "summary": "За курсором тянется магнитный шлейф из восьми градиентных «миниатюр»: каждая запаздывает всё сильнее и слегка доворачивается по направлению движения.",
      "effect": "Позиции указателя пишутся в историю с шагом ≥9 px (без накопления дублей при остановке); плитка i стремится к точке hist[i×6] с lerp 0.24 — восемь миниатюр растягиваются вдоль пройденного пути, затухая по масштабу (1 − i×0.06) и прозрачности (1 − i×0.09) и доворачиваясь от локальной скорости. Когда курсор замирает или его нет 0.65 с, авто-демо ведёт указатель по лиссажу-траектории. При reduced-motion плитки выстроены статичной лентой.",
      "when_to_use": "Портфолио и студии, галереи работ, креативные промо, обложки кейсов — flair-эффект для курсора на десктопе.",
      "tech": [
        "vanilla-js",
        "css"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<div class=\"trail-stage\" id=\"trailStage\">\n  <span class=\"trail-img\" style=\"--i:0\"></span><span class=\"trail-img\" style=\"--i:1\"></span><span class=\"trail-img\" style=\"--i:2\"></span><span class=\"trail-img\" style=\"--i:3\"></span><span class=\"trail-img\" style=\"--i:4\"></span><span class=\"trail-img\" style=\"--i:5\"></span><span class=\"trail-img\" style=\"--i:6\"></span><span class=\"trail-img\" style=\"--i:7\"></span>\n  <p class=\"trail-cap\">Move cursor · magnetic trail</p>\n</div>",
        "css": ".trail-stage { position: relative; width: 100%; height: 320px; overflow: hidden; background: radial-gradient(120% 120% at 60% 20%, #151b2b 0%, #0b0d12 60%); cursor: crosshair; }\n.trail-img { position: absolute; top: 0; left: 0; width: 54px; height: 54px; border-radius: 15px; border: 1px solid rgba(255, 255, 255, .3); box-shadow: 0 12px 28px rgba(0, 0, 0, .45); opacity: 0; will-change: transform; background: linear-gradient(140deg, hsl(calc(var(--i) * 38deg + 215deg) 85% 64%), hsl(calc(var(--i) * 38deg + 258deg) 72% 44%)); }\n.trail-cap { position: absolute; left: 16px; bottom: 12px; margin: 0; font: 600 10px/1 ui-monospace, monospace; letter-spacing: .16em; text-transform: uppercase; color: #6f7796; }\n@media (prefers-reduced-motion: reduce) { .trail-img { opacity: 1; } }",
        "js": "const stage = document.getElementById(\"trailStage\");\nconst nodes = [...stage.querySelectorAll(\".trail-img\")];\nconst reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nconst hist = [];\nconst state = nodes.map(() => ({ x: 240, y: 160 }));\nlet px = 240, py = 160, last = 0, auto = true;\nconst t0 = performance.now();\n\nfunction record(x, y) {\n  const head = hist[0];\n  if (!head || Math.hypot(x - head.x, y - head.y) > 9) hist.unshift({ x, y });\n  if (hist.length > 60) hist.pop();\n}\nfor (let i = 0; i < 40; i++) record(70 + i * 9, 236 - i * 4.2);\n\nstage.addEventListener(\"pointermove\", (e) => {\n  const rect = stage.getBoundingClientRect();\n  px = e.clientX - rect.left; py = e.clientY - rect.top;\n  last = performance.now(); auto = false;\n});\n\nif (reduce) {\n  nodes.forEach((n, i) => {\n    n.style.transform = \"translate(\" + (36 + i * 52) + \"px, 140px) rotate(\" + (i * 6 - 20) + \"deg)\";\n  });\n} else {\n  (function loop(now) {\n    if (auto || now - last > 650) {\n      auto = true;\n      px = stage.clientWidth / 2 + Math.sin((now - t0) / 780) * 150;\n      py = stage.clientHeight / 2 + Math.sin((now - t0) / 520) * 70;\n    }\n    record(px, py);\n    nodes.forEach((n, i) => {\n      const p = hist[Math.min(hist.length - 1, i * 6)];\n      const s = state[i];\n      s.x += (p.x - s.x) * 0.24;\n      s.y += (p.y - s.y) * 0.24;\n      const spin = Math.max(-16, Math.min(16, (p.x - s.x) * 0.9));\n      n.style.opacity = String(1 - i * 0.09);\n      n.style.transform = \"translate(\" + (s.x - 27) + \"px,\" + (s.y - 27) + \"px) scale(\" + (1 - i * 0.06) + \") rotate(\" + spin + \"deg)\";\n    });\n    requestAnimationFrame(loop);\n  })(performance.now());\n}"
      },
      "source": {
        "name": "MDN — Element: pointermove event",
        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Element/pointermove_event",
        "domain": "developer.mozilla.org"
      },
      "performance": "8 элементов, только transform/opacity; буфер истории фиксированной длины (90 точек), rAF один на блок.",
      "a11y": "prefers-reduced-motion: reduce — rAF-шлейф не запускается, восемь плиток статично расставлены лентой.",
      "browser_support": "Pointer events + rAF + CSS calc() в hsl(): Chrome 62+, Safari 12+, Firefox 58+ — все актуальные.",
      "ai_use": "Use for portfolio flair on desktop: push cursor positions into a history only every >=9px of movement, let 8 gradient tiles chase hist[i*6] with 0.24 lerp (they hold their spread when the pointer stops), rotate by local velocity, fade 9% per tile; drive the pointer along a lissajous path when idle so recordings show motion.",
      "preview": {
        "scroll": false,
        "mouse": ".trail-stage",
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-07-physics3d.json"
    },
    {
      "id": "tech-intersection-observer-reveal",
      "name": "IntersectionObserver Reveal",
      "name_ru": "Появление на скролле через IntersectionObserver",
      "category": "technique",
      "tags": [
        "on-scroll",
        "reveal",
        "intersection-observer",
        "vanilla-js",
        "data-attribute"
      ],
      "summary": "Мини-утилита: любой элемент с data-reveal появляется при входе во вьюпорт — opacity 0→1 и подъём снизу на 18px. Один общий observer на страницу, после показа элемент отписывается.",
      "effect": "Старт: opacity 0, translateY(18px). При пересечении порога 20% с rootMargin '0px 0px -10% 0px' добавляется класс .is-visible: переход 600ms cubic-bezier(.2,.7,.2,1), задержка растёт на 80ms по индексу (--reveal-i). После срабатывания unobserve — одноразовое появление.",
      "when_to_use": "Базовая механика для секций лендинга: заголовки, карточки фич, логотипы, CTA. Первый инструмент, когда не нужны таймлайны и scrub-эффекты.",
      "tech": [
        "vanilla-js",
        "css",
        "intersection-observer"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <h2 class=\"demo__title\" data-reveal>Launch faster</h2>\n  <p class=\"demo__sub\" data-reveal>Motion that feels alive, not decorative.</p>\n  <div class=\"demo__row\">\n    <div class=\"demo__card\" data-reveal>Design</div>\n    <div class=\"demo__card\" data-reveal>Build</div>\n    <div class=\"demo__card\" data-reveal>Ship</div>\n  </div>\n</section>",
        "css": ".demo {\n  box-sizing: border-box;\n  min-height: 420px;\n  padding: 28px 24px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__title { margin: 0 0 6px; font-size: 26px; }\n.demo__sub { margin: 0 0 22px; color: #98a2b8; font-size: 14px; }\n.demo__row { display: grid; gap: 12px; }\n.demo__card {\n  padding: 18px;\n  border-radius: 14px;\n  background: linear-gradient(135deg, #1a2030, #11151f);\n  border: 1px solid #2a3246;\n}\n[data-reveal] {\n  opacity: 0;\n  transform: translateY(18px);\n  transition: opacity .6s cubic-bezier(.2,.7,.2,1), transform .6s cubic-bezier(.2,.7,.2,1);\n  transition-delay: calc(var(--reveal-i, 0) * 80ms);\n}\n[data-reveal].is-visible { opacity: 1; transform: none; }\n@media (prefers-reduced-motion: reduce) {\n  [data-reveal] { transition: none; opacity: 1; transform: none; }\n}",
        "js": "function initReveal(root = document) {\n  const els = [...root.querySelectorAll(\"[data-reveal]\")];\n  const reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  if (reduced || !(\"IntersectionObserver\" in window)) {\n    els.forEach((el) => el.classList.add(\"is-visible\"));\n    return;\n  }\n  const io = new IntersectionObserver(\n    (entries) => {\n      for (const entry of entries) {\n        if (!entry.isIntersecting) continue;\n        entry.target.classList.add(\"is-visible\");\n        io.unobserve(entry.target);\n      }\n    },\n    { threshold: 0.2, rootMargin: \"0px 0px -10% 0px\" }\n  );\n  els.forEach((el, i) => {\n    el.style.setProperty(\"--reveal-i\", i);\n    io.observe(el);\n  });\n  return io;\n}\n\ninitReveal();"
      },
      "source": {
        "name": "MDN — IntersectionObserver",
        "url": "https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver",
        "domain": "developer.mozilla.org"
      },
      "performance": "Только opacity/transform — compositor-friendly. Один observer вместо N, unobserve после показа. Не анимируйте так height/filter/box-shadow.",
      "a11y": "matchMedia('(prefers-reduced-motion: reduce)') ставит .is-visible сразу без анимации; CSS-медиазапрос дополнительно обнуляет transition.",
      "browser_support": "Chrome 51+, Safari 12.1+, Firefox 55+ (IntersectionObserver widely available).",
      "ai_use": "Add data-reveal to landing blocks, init one IntersectionObserver that adds .is-visible at 20% visibility: 600ms cubic-bezier(.2,.7,.2,1), 80ms stagger per item.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-waapi-keyframes",
      "name": "Web Animations API Keyframes",
      "name_ru": "Кейфреймы через Web Animations API",
      "category": "technique",
      "tags": [
        "entrance",
        "keyframes",
        "waapi",
        "vanilla-js",
        "pulse"
      ],
      "summary": "element.animate() запускает keyframe-анимацию с овершутом на карточке тарифа и бесконечный пульс статус-точки — без библиотек и CSS-классов.",
      "effect": "Вход карточки: 700ms cubic-bezier(.2,.7,.2,1), три кейфрейма — из translateY(24px) scale(.96) через лёгкий перелёт (offset .7) в 0/1, fill forwards. Пульс точки: scale 1→2.4 с падением opacity, 1400ms ease-out, iterations Infinity.",
      "when_to_use": "Точечные анимации на JavaScript без библиотеки: карточки тарифов, бейджи, статус-индикаторы, а также цепочки через animation.finished (Promise).",
      "tech": [
        "vanilla-js",
        "css",
        "web-animations"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <article class=\"demo__card\">\n    <span class=\"demo__dot\"></span>\n    <h2 class=\"demo__title\">Pro plan</h2>\n    <p class=\"demo__price\">$29<span>/mo</span></p>\n    <button class=\"demo__btn\">Start free trial</button>\n  </article>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  display: grid;\n  place-items: center;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__card {\n  width: 280px;\n  padding: 22px;\n  border-radius: 18px;\n  background: #151a26;\n  border: 1px solid #2a3246;\n  opacity: 0;\n}\n.demo__dot {\n  display: block;\n  width: 10px;\n  height: 10px;\n  margin-bottom: 12px;\n  border-radius: 50%;\n  background: #37e28b;\n}\n.demo__title { margin: 0; font-size: 20px; }\n.demo__price { margin: 8px 0 16px; font-size: 34px; }\n.demo__price span { font-size: 14px; color: #98a2b8; }\n.demo__btn {\n  width: 100%;\n  padding: 10px 14px;\n  border: 0;\n  border-radius: 10px;\n  background: #5b7cfa;\n  color: #fff;\n  font: inherit;\n}\n@media (prefers-reduced-motion: reduce) {\n  .demo__card { opacity: 1; }\n}",
        "js": "const card = document.querySelector(\".demo__card\");\nconst dot = document.querySelector(\".demo__dot\");\nconst reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nif (!reduced) {\n  card.animate(\n    [\n      { opacity: 0, transform: \"translateY(24px) scale(.96)\" },\n      { opacity: 1, transform: \"translateY(-4px) scale(1.01)\", offset: 0.7 },\n      { opacity: 1, transform: \"translateY(0) scale(1)\" }\n    ],\n    { duration: 700, easing: \"cubic-bezier(.2,.7,.2,1)\", fill: \"forwards\" }\n  );\n\n  dot.animate(\n    [\n      { transform: \"scale(1)\", opacity: 0.9 },\n      { transform: \"scale(2.4)\", opacity: 0 }\n    ],\n    { duration: 1400, iterations: Infinity, easing: \"ease-out\" }\n  );\n}"
      },
      "source": {
        "name": "MDN — Element.animate()",
        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Element/animate",
        "domain": "developer.mozilla.org"
      },
      "performance": "transform/opacity — compositor-only. fill forwards оставляет завершённые анимации в списке getAnimations(); на массовых элементах вызывайте cancel().",
      "a11y": "Анимации создаются только если prefers-reduced-motion не включён (проверка matchMedia); CSS при reduced сразу показывает карточку (opacity 1).",
      "browser_support": "Chrome 36+, Safari 13.1+, Firefox 48+ (Element.animate widely available).",
      "ai_use": "Animate pricing cards with el.animate(): 700ms cubic-bezier(.2,.7,.2,1) three-keyframe rise with slight overshoot (offset .7), fill forwards; add an infinite scale+fade pulse on the status dot.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-prefers-reduced-motion",
      "name": "prefers-reduced-motion Pattern",
      "name_ru": "Паттерн prefers-reduced-motion",
      "category": "technique",
      "tags": [
        "a11y",
        "reduced-motion",
        "matchmedia",
        "looping",
        "pattern"
      ],
      "summary": "Единый паттерн: CSS-медиазапрос выключает анимацию, а JS через matchMedia слушает смену настройки вживую и синхронизирует атрибут data-motion на <html>.",
      "effect": "Орб плавает 2400ms ease-in-out infinite (translateY −16px + rotate 8deg). При reduced-режиме animation снимается — элемент замирает; индикатор режима меняется full/reduced. Демо-цикл сам переключает оба состояния каждые 2.2s.",
      "when_to_use": "Обязательная обвязка для любого лендинга с анимациями: положите её в базовый CSS-шаблон и в общий gate-хелпер, чтобы все секции уважали настройку ОС.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <div class=\"demo__orb\"></div>\n  <h2 class=\"demo__title\">Motion, on your terms</h2>\n  <p class=\"demo__state\">mode: <b class=\"demo__mode\">full</b></p>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  display: grid;\n  place-content: center;\n  justify-items: center;\n  gap: 18px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n  text-align: center;\n}\n.demo__orb {\n  width: 72px;\n  height: 72px;\n  border-radius: 24px 24px 24px 4px;\n  background: linear-gradient(135deg, #5b7cfa, #b06ef5);\n  animation: float 2.4s ease-in-out infinite;\n}\n@keyframes float {\n  0%, 100% { transform: translateY(0) rotate(0deg); }\n  50% { transform: translateY(-16px) rotate(8deg); }\n}\n.demo__title { margin: 0; font-size: 20px; }\n.demo__state { margin: 0; color: #98a2b8; font-size: 13px; }\n[data-motion=\"reduced\"] .demo__orb { animation: none; }\n@media (prefers-reduced-motion: reduce) {\n  .demo__orb { animation: none; }\n}",
        "js": "const root = document.documentElement;\nconst mq = matchMedia(\"(prefers-reduced-motion: reduce)\");\nconst mode = document.querySelector(\".demo__mode\");\n\nfunction syncMotionMode() {\n  const reduced = mq.matches || root.dataset.simulateReduced === \"true\";\n  root.dataset.motion = reduced ? \"reduced\" : \"full\";\n  mode.textContent = reduced ? \"reduced\" : \"full\";\n}\n\nmq.addEventListener(\"change\", syncMotionMode);\nsyncMotionMode();\n\n// demo only: flip a simulated preference so both states are visible\nsetInterval(() => {\n  root.dataset.simulateReduced = root.dataset.simulateReduced === \"true\" ? \"false\" : \"true\";\n  syncMotionMode();\n}, 2200);"
      },
      "source": {
        "name": "MDN — prefers-reduced-motion",
        "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion",
        "domain": "developer.mozilla.org"
      },
      "performance": "Одна анимация на transform; при reduced полностью снимается браузером по медиазапросу, без JS-работы в кадре.",
      "a11y": "Ядро карточки: @media (prefers-reduced-motion: reduce) даёт статичный фолбэк; JS matchMedia + слушатель change переключает режим на лету для скриптов.",
      "browser_support": "Chrome 74+, Safari 10.1+, Firefox 63+ (медиазапрос); matchMedia widely available.",
      "ai_use": "Wrap every landing animation in this gate: sync prefers-reduced-motion to a data-motion attribute on <html>, disable CSS keyframes and skip JS animation setup when reduced.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2400,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-flip-layout",
      "name": "FLIP Layout Transition",
      "name_ru": "FLIP-переход раскладки",
      "category": "technique",
      "tags": [
        "flip",
        "layout",
        "reorder",
        "web-animations",
        "vanilla-js"
      ],
      "summary": "Плитки меняют порядок в DOM, а FLIP-хелпер анимирует перестроение: замер позиций ДО, мутация, перевод каждой плитки из старой точки в новую через translate.",
      "effect": "Каждые 1800ms первая плитка уезжает в конец; позиции замеряются getBoundingClientRect до и после мутации, затем каждая плитка проигрывает translate(dx,dy)→0 за 450ms cubic-bezier(.2,.7,.2,1). Layout при этом не анимируется.",
      "when_to_use": "Списки, сетки, фильтры и сортировки: перестановка шагов процесса, кейсов, карточек команды; любые смены раскладки без резких скачков.",
      "tech": [
        "vanilla-js",
        "web-animations"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class=\"demo\">\n  <h2 class=\"demo__title\">FLIP reorder</h2>\n  <div class=\"demo__row\">\n    <div class=\"demo__tile\">Research</div>\n    <div class=\"demo__tile\">Design</div>\n    <div class=\"demo__tile\">Build</div>\n    <div class=\"demo__tile\">Ship</div>\n  </div>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  padding: 26px 20px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__title { margin: 0 0 18px; font-size: 20px; }\n.demo__row {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 12px;\n}\n.demo__tile {\n  flex: 1 1 calc(50% - 12px);\n  padding: 22px 16px;\n  border-radius: 14px;\n  background: #182031;\n  border: 1px solid #2a3246;\n  font-size: 14px;\n}\n@media (prefers-reduced-motion: reduce) {\n  .demo__tile { transition: none !important; }\n}",
        "js": "const row = document.querySelector(\".demo__row\");\nconst tiles = [...row.children];\nconst reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nfunction flip(mutate) {\n  const first = new Map(tiles.map((el) => [el, el.getBoundingClientRect()]));\n  mutate();\n  for (const el of tiles) {\n    const before = first.get(el);\n    const after = el.getBoundingClientRect();\n    const dx = before.left - after.left;\n    const dy = before.top - after.top;\n    if (!dx && !dy) continue;\n    if (reduced) continue;\n    el.animate(\n      [{ transform: \"translate(\" + dx + \"px,\" + dy + \"px)\" }, { transform: \"translate(0,0)\" }],\n      { duration: 450, easing: \"cubic-bezier(.2,.7,.2,1)\" }\n    );\n  }\n}\n\nsetInterval(() => flip(() => row.appendChild(row.firstElementChild)), 1800);"
      },
      "source": {
        "name": "CSS-Tricks — Animating Layouts with the FLIP Technique",
        "url": "https://css-tricks.com/animating-layouts-with-the-flip-technique/",
        "domain": "css-tricks.com"
      },
      "performance": "Анимируется только transform; два getBoundingClientRect на цикл вызывают reflow — дёшево для десятков элементов, избегайте на сотнях узлов в rAF.",
      "a11y": "При prefers-reduced-motion DOM-мутация выполняется мгновенно (WAAPI-вызов пропускается); CSS-медиазапрос глушит transition на плитках.",
      "browser_support": "Chrome 80+, Safari 14+, Firefox 80+ (стабильные getBoundingClientRect + Element.animate).",
      "ai_use": "For reorderable grids use FLIP: measure rects before DOM mutation, then el.animate() each moved item back from its old offset (450ms cubic-bezier(.2,.7,.2,1)); skip animation under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2000,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-compositor-performance",
      "name": "Compositor-only Performance Patterns",
      "name_ru": "Производительность: compositor-only, will-change, content-visibility",
      "category": "technique",
      "tags": [
        "performance",
        "compositor",
        "will-change",
        "transform",
        "content-visibility"
      ],
      "summary": "Набор приёмов для тяжёлых лендингов: бегущая строка и hover-подъём анимируют только transform/opacity, will-change включается лишь на время анимации, нижний блок рендерится лениво через content-visibility.",
      "effect": "Маркиза едет translateX 0→−50% линейно 14s infinite. Карточка на hover поднимается translateY(−6px) scale(1.02) за 350ms cubic-bezier(.2,.7,.2,1). will-change:transform ставится на pointerenter и снимается на transitionend — бюджет слоёв не растёт постоянно.",
      "when_to_use": "Аудит и разметка тяжёлых страниц: бегущие строки, hover-карточки, длинные секции ниже первого экрана, где важен стабильный FPS.",
      "tech": [
        "css",
        "vanilla-js"
      ],
      "dependencies": [],
      "difficulty": "medium",
      "code": {
        "html": "<section class=\"demo\">\n  <p class=\"demo__hint\">transform / opacity only · will-change on demand</p>\n  <div class=\"demo__marquee\">\n    <div class=\"demo__strip\">\n      <span>Design</span><span>Build</span><span>Ship</span><span>Measure</span>\n      <span>Design</span><span>Build</span><span>Ship</span><span>Measure</span>\n    </div>\n  </div>\n  <article class=\"demo__card\">\n    <h3 class=\"demo__name\">Compositor card</h3>\n    <p class=\"demo__text\">Hover: transform lift, no layout triggered.</p>\n  </article>\n  <section class=\"demo__offscreen\">\n    <h3>content-visibility: auto</h3>\n    <p>Rendering of this block is skipped until it approaches the viewport.</p>\n  </section>\n</section>",
        "css": ".demo {\n  min-height: 620px;\n  padding: 22px 20px 60px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__hint { margin: 0 0 12px; font-size: 12px; color: #98a2b8; }\n.demo__marquee {\n  overflow: hidden;\n  border-radius: 12px;\n  background: #121826;\n  padding: 14px 0;\n}\n.demo__strip {\n  display: flex;\n  gap: 28px;\n  width: max-content;\n  will-change: transform;\n  animation: slide 14s linear infinite;\n}\n.demo__strip span { font-size: 15px; color: #9fb0ff; }\n@keyframes slide { to { transform: translateX(-50%); } }\n.demo__card {\n  margin-top: 16px;\n  padding: 18px;\n  border-radius: 14px;\n  background: #182031;\n  transition: transform .35s cubic-bezier(.2,.7,.2,1);\n}\n.demo__card:hover { transform: translateY(-6px) scale(1.02); }\n.demo__offscreen {\n  margin-top: 300px;\n  padding: 18px;\n  border-radius: 14px;\n  background: #182031;\n  content-visibility: auto;\n  contain-intrinsic-size: auto 180px;\n}\n@media (prefers-reduced-motion: reduce) {\n  .demo__strip { animation: none; }\n}",
        "js": "const card = document.querySelector(\".demo__card\");\n\n// keep will-change only while the transform is actually animating\ncard.addEventListener(\"pointerenter\", () => {\n  card.style.willChange = \"transform\";\n});\ncard.addEventListener(\"transitionend\", () => {\n  card.style.willChange = \"auto\";\n});"
      },
      "source": {
        "name": "web.dev — Stick to Compositor-Only Properties and Manage Layer Count",
        "url": "https://web.dev/articles/stick-to-compositor-only-properties-and-manage-layer-count",
        "domain": "web.dev"
      },
      "performance": "Все движения — transform/opacity (compositor-only). content-visibility:auto + contain-intrinsic-size пропускает рендер невидимых секций; will-change висит ограниченно, чтобы не плодить слои и память.",
      "a11y": "При prefers-reduced-motion маркиза останавливается (animation:none), hover-переходы остаются без движения; контент полностью доступен.",
      "browser_support": "content-visibility: Chrome 85+, Safari 18+ (graceful degradation в остальных); will-change: Chrome 36+, Safari 9.1+, Firefox 36+.",
      "ai_use": "Optimize landing performance: animate only transform/opacity, gate will-change on pointerenter/transitionend, and add content-visibility:auto with contain-intrinsic-size to below-the-fold sections.",
      "preview": {
        "scroll": true,
        "mouse": ".demo__card",
        "wait_ms": 3000,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-gsap-timeline",
      "name": "GSAP Timeline Sequencing",
      "name_ru": "GSAP Timeline: секвенция и stagger",
      "category": "technique",
      "tags": [
        "gsap",
        "timeline",
        "stagger",
        "hero",
        "looping"
      ],
      "summary": "Таймлайн GSAP с общими defaults собирает вход hero-блока: eyebrow, заголовок, подзаголовок и две кнопки со stagger; цикл повторяется, чтобы сцену было видно в записи.",
      "effect": "defaults: duration 600ms, ease power3.out; шаги накладываются позициями '−=0.35', кнопки входят со stagger 120ms из y 14 + autoAlpha 0. Пауза 1.5s и полный повтор (repeat -1, repeatDelay 1.5).",
      "when_to_use": "Hero, заголовки секций, онбординг-сцены — любые цепочки 3+ элементов, где нужен единый ритм и правка таймингов в одном месте.",
      "tech": [
        "gsap"
      ],
      "dependencies": [
        "gsap 3.13.0 (CDN)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\"></script>\n<section class=\"demo\">\n  <p class=\"demo__eyebrow\">New</p>\n  <h2 class=\"demo__title\">Aurora Analytics</h2>\n  <p class=\"demo__sub\">Product analytics in one afternoon, not one quarter.</p>\n  <div class=\"demo__actions\">\n    <button class=\"demo__btn\">Get started</button>\n    <button class=\"demo__btn demo__btn--ghost\">Watch demo</button>\n  </div>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  padding: 30px 24px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__eyebrow {\n  margin: 0;\n  color: #7d8cff;\n  font-size: 12px;\n  letter-spacing: .12em;\n  text-transform: uppercase;\n}\n.demo__title { margin: 6px 0 10px; font-size: 28px; }\n.demo__sub { margin: 0 0 22px; color: #98a2b8; max-width: 32ch; }\n.demo__actions { display: flex; gap: 10px; }\n.demo__btn {\n  padding: 11px 18px;\n  border: 0;\n  border-radius: 10px;\n  background: #5b7cfa;\n  color: #fff;\n  font: inherit;\n}\n.demo__btn--ghost {\n  background: transparent;\n  border: 1px solid #2f3a52;\n  color: #c9d2e4;\n}\n@media (prefers-reduced-motion: reduce) {\n  .demo__eyebrow, .demo__title, .demo__sub, .demo__btn { visibility: visible; opacity: 1; }\n}",
        "js": "const reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nif (!reduced) {\n  const tl = gsap.timeline({\n    defaults: { duration: 0.6, ease: \"power3.out\" },\n    repeat: -1,\n    repeatDelay: 1.5\n  });\n\n  tl.from(\".demo__eyebrow\", { y: 14, autoAlpha: 0 })\n    .from(\".demo__title\", { y: 30, autoAlpha: 0 }, \"-=0.35\")\n    .from(\".demo__sub\", { y: 18, autoAlpha: 0 }, \"-=0.4\")\n    .from(\".demo__btn\", { y: 14, autoAlpha: 0, stagger: 0.12 }, \"-=0.35\");\n}"
      },
      "source": {
        "name": "GSAP Docs — Timeline",
        "url": "https://gsap.com/docs/v3/GSAP/Timeline/",
        "domain": "gsap.com"
      },
      "performance": "GSAP пишет transform/opacity в inline-стили — compositor-friendly; autoAlpha попутно управляет visibility. Один таймлайн на блок, без rAF-циклов в коде.",
      "a11y": "matchMedia-гейт: при prefers-reduced-motion таймлайн не создаётся; CSS-медиазапрос оставляет элементы видимыми.",
      "browser_support": "Chrome 60+, Safari 12+, Firefox 60+ (GSAP 3).",
      "ai_use": "Build a hero entrance timeline: defaults {duration:.6, ease:'power3.out'}, sequence eyebrow → title → sub → buttons with overlapping '-=0.35' positions and stagger 0.12 on buttons.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-gsap-scrolltrigger",
      "name": "GSAP ScrollTrigger Reveal & Scrub",
      "name_ru": "GSAP ScrollTrigger: reveal и scrub",
      "category": "technique",
      "tags": [
        "gsap",
        "scrolltrigger",
        "on-scroll",
        "scrub",
        "once"
      ],
      "summary": "Карточки шагов появляются группами при входе в зону (start 'top 88%', once:true), а прогресс-полоса над ними скраббится по позиции скролла.",
      "effect": "Reveal: y 60→0 + autoAlpha 0→1, 700ms power3.out, stagger 150ms на группу, триггер срабатывает один раз. Полоса: scaleX 0→1, ease none, привязана к прогрессу секции (scrub 0.4) — движение линейно позиции скролла.",
      "when_to_use": "Секции возможностей, шаги процесса, длинные лендинги с прогрессом; scrub — для прогресс-баров, параллакса и залипающих сцен.",
      "tech": [
        "gsap",
        "scrolltrigger"
      ],
      "dependencies": [
        "gsap 3.13.0 + ScrollTrigger (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js\"></script>\n<section class=\"demo\">\n  <h2 class=\"demo__title\">How it works</h2>\n  <div class=\"demo__bar\"><i></i></div>\n  <div class=\"demo__list\">\n    <article class=\"demo__card\"><h3>1 · Connect</h3><p>Plug your data sources in minutes.</p></article>\n    <article class=\"demo__card\"><h3>2 · Model</h3><p>Define metrics without SQL.</p></article>\n    <article class=\"demo__card\"><h3>3 · Share</h3><p>Publish live dashboards.</p></article>\n  </div>\n</section>",
        "css": ".demo {\n  min-height: 760px;\n  padding: 26px 22px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__title { margin: 0 0 14px; font-size: 24px; }\n.demo__bar {\n  height: 4px;\n  border-radius: 4px;\n  background: #1c2333;\n  overflow: hidden;\n}\n.demo__bar i {\n  display: block;\n  height: 100%;\n  background: linear-gradient(90deg, #5b7cfa, #b06ef5);\n  transform: scaleX(0);\n  transform-origin: left center;\n}\n.demo__list { display: grid; gap: 14px; margin-top: 26px; }\n.demo__card {\n  padding: 18px;\n  border-radius: 14px;\n  background: #182031;\n  border: 1px solid #2a3246;\n}\n.demo__card h3 { margin: 0 0 6px; font-size: 16px; }\n.demo__card p { margin: 0; color: #98a2b8; font-size: 14px; }\n@media (prefers-reduced-motion: reduce) {\n  .demo__bar i { transform: scaleX(1); }\n}",
        "js": "gsap.registerPlugin(ScrollTrigger);\nconst reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nif (!reduced) {\n  gsap.from(\".demo__card\", {\n    y: 60,\n    autoAlpha: 0,\n    duration: 0.7,\n    ease: \"power3.out\",\n    stagger: 0.15,\n    scrollTrigger: { trigger: \".demo__list\", start: \"top 88%\", once: true }\n  });\n\n  gsap.fromTo(\n    \".demo__bar i\",\n    { scaleX: 0 },\n    {\n      scaleX: 1,\n      ease: \"none\",\n      scrollTrigger: { trigger: \".demo\", start: \"top top\", end: \"bottom bottom\", scrub: 0.4 }\n    }\n  );\n}"
      },
      "source": {
        "name": "GSAP Docs — ScrollTrigger",
        "url": "https://gsap.com/docs/v3/Plugins/ScrollTrigger/",
        "domain": "gsap.com"
      },
      "performance": "Анимации transform/opacity; один ScrollTrigger на группу, once:true снимает слушатели после срабатывания. scrub работает на общем тикере GSAP.",
      "a11y": "При prefers-reduced-motion триггеры не создаются: карточки видны сразу, полоса заполнена на 100% (CSS).",
      "browser_support": "Chrome 60+, Safari 12+, Firefox 60+.",
      "ai_use": "Reveal feature cards with ScrollTrigger {trigger, start:'top 88%', once:true} + y/autoAlpha stagger 0.15; drive a progress bar with a second ScrollTrigger using scrub:0.4.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3200,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-gsap-splittext",
      "name": "GSAP SplitText Character Reveal",
      "name_ru": "GSAP SplitText: посимвольный вход",
      "category": "technique",
      "tags": [
        "gsap",
        "splittext",
        "text",
        "stagger",
        "mask",
        "looping"
      ],
      "summary": "SplitText режет заголовок на символы и оборачивает их в маски; символы выезжают снизу со stagger, цикл повторяется.",
      "effect": "Каждый char стартует из yPercent 130 с rotate 4deg под маской (clip), выезжает 800ms power4.out со stagger 28ms от начала строки; пауза 1.6s и полный повтор.",
      "when_to_use": "Крупные hero-заголовки, анонсы запуска, цитаты — когда нужен дорогой текстовый вход без 3D и WebGL.",
      "tech": [
        "gsap",
        "splittext"
      ],
      "dependencies": [
        "gsap 3.13.0 + SplitText (CDN; с 3.13 плагин бесплатный)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js\"></script>\n<section class=\"demo\">\n  <h2 class=\"demo__title\">Motion in every pixel</h2>\n  <p class=\"demo__sub\">SplitText ships free with GSAP 3.13+.</p>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  display: grid;\n  align-content: center;\n  gap: 14px;\n  padding: 0 26px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__title {\n  margin: 0;\n  font-size: 40px;\n  line-height: 1.05;\n  letter-spacing: -0.02em;\n  font-kerning: none;\n  text-rendering: optimizeSpeed;\n}\n.demo__sub { margin: 0; color: #98a2b8; font-size: 14px; }\n@media (prefers-reduced-motion: reduce) {\n  .demo__title { letter-spacing: normal; }\n}",
        "js": "gsap.registerPlugin(SplitText);\nconst reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nif (!reduced) {\n  const split = SplitText.create(\".demo__title\", {\n    type: \"chars\",\n    mask: \"chars\",\n    charsClass: \"char\"\n  });\n\n  gsap.from(split.chars, {\n    yPercent: 130,\n    rotate: 4,\n    duration: 0.8,\n    ease: \"power4.out\",\n    stagger: { each: 0.028, from: \"start\" },\n    repeat: -1,\n    repeatDelay: 1.6\n  });\n}"
      },
      "source": {
        "name": "GSAP Docs — SplitText",
        "url": "https://gsap.com/docs/v3/Plugins/SplitText/",
        "domain": "gsap.com"
      },
      "performance": "Сотни DOM-узлов на заголовок: split'ите только видимый текст (type 'chars' без words), анимация transform-only; после показа можно вызвать split.revert().",
      "a11y": "SplitText сам ставит aria-label на заголовок и aria-hidden на осколки (aria:'auto'); при prefers-reduced-motion split не создаётся вовсе.",
      "browser_support": "Chrome 60+, Safari 12+, Firefox 60+.",
      "ai_use": "For big headlines: SplitText.create(el, {type:'chars', mask:'chars'}) then gsap.from(chars, {yPercent:130, rotate:4, duration:.8, ease:'power4.out', stagger:.028}).",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2600,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-motion-vanilla",
      "name": "Motion Vanilla Stagger & Spring",
      "name_ru": "Motion (ex-Framer Motion) без фреймворка",
      "category": "technique",
      "tags": [
        "motion",
        "spring",
        "stagger",
        "looping",
        "cdn"
      ],
      "summary": "Motion из CDN анимирует эквалайзер из баров волной (stagger + reverse loop) и выводит карточку пружиной — тот же движок, что у Framer Motion, но без React.",
      "effect": "Бары: scaleY 0.3→1 за 900ms с delay = stagger(70ms), repeat Infinity и repeatType 'reverse', ease cubic-bezier(.22,1,.36,1). Карточка входит spring-анимацией stiffness 210, damping 22 из y 26 и opacity 0.",
      "when_to_use": "Hero-графика, живые метрики и эквалайзеры, карточки с пружинным входом; удобно командам, знающим Framer Motion API.",
      "tech": [
        "motion",
        "vanilla-js"
      ],
      "dependencies": [
        "motion 13.3 (CDN, глобал Motion)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/motion@13.3.0/dist/motion.js\"></script>\n<section class=\"demo\">\n  <div class=\"demo__bars\">\n    <span class=\"demo__bar\"></span><span class=\"demo__bar\"></span><span class=\"demo__bar\"></span>\n    <span class=\"demo__bar\"></span><span class=\"demo__bar\"></span><span class=\"demo__bar\"></span>\n    <span class=\"demo__bar\"></span><span class=\"demo__bar\"></span><span class=\"demo__bar\"></span>\n    <span class=\"demo__bar\"></span><span class=\"demo__bar\"></span><span class=\"demo__bar\"></span>\n  </div>\n  <div class=\"demo__card\">\n    <h2 class=\"demo__title\">Springs by default</h2>\n    <p class=\"demo__sub\">The same engine as Framer Motion, in plain JS.</p>\n  </div>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  display: grid;\n  align-content: center;\n  gap: 22px;\n  padding: 0 26px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__bars {\n  display: flex;\n  align-items: flex-end;\n  gap: 6px;\n  height: 90px;\n}\n.demo__bar {\n  flex: 1;\n  height: 100%;\n  border-radius: 6px;\n  background: linear-gradient(180deg, #5b7cfa, #2c3d8f);\n  transform-origin: bottom center;\n}\n.demo__card {\n  padding: 18px;\n  border-radius: 16px;\n  background: #182031;\n  border: 1px solid #2a3246;\n}\n.demo__title { margin: 0 0 6px; font-size: 20px; }\n.demo__sub { margin: 0; color: #98a2b8; font-size: 14px; }\n@media (prefers-reduced-motion: reduce) {\n  .demo__bar { transform: scaleY(1) !important; }\n}",
        "js": "const { animate, stagger } = Motion;\nconst reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nif (!reduced) {\n  animate(\n    \".demo__bar\",\n    { scaleY: [0.3, 1], opacity: [0.5, 1] },\n    {\n      duration: 0.9,\n      delay: stagger(0.07),\n      repeat: Infinity,\n      repeatType: \"reverse\",\n      ease: [0.22, 1, 0.36, 1]\n    }\n  );\n\n  animate(\n    \".demo__card\",\n    { y: [26, 0], opacity: [0, 1] },\n    { type: \"spring\", stiffness: 210, damping: 22 }\n  );\n}"
      },
      "source": {
        "name": "Motion Docs — Quick start (script tag)",
        "url": "https://motion.dev/docs/quick-start",
        "domain": "motion.dev"
      },
      "performance": "Motion сам выбирает hardware-accelerated свойства (transform/opacity/filter); stagger — функция задержек, один animate-вызов на группу.",
      "a11y": "При prefers-reduced-motion код не вызывает animate(); CSS-медиазапрос сразу даёт барам финальный scaleY(1).",
      "browser_support": "Chrome 80+, Safari 14+, Firefox 80+.",
      "ai_use": "With Motion from CDN animate a bar equalizer: scaleY [0.3,1] with delay stagger(0.07), repeat Infinity + repeatType 'reverse'; pop the card in with {type:'spring', stiffness:210}.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2400,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-anime-stagger-grid",
      "name": "Anime.js Grid Stagger",
      "name_ru": "Anime.js: grid-stagger по сетке",
      "category": "technique",
      "tags": [
        "animejs",
        "stagger",
        "grid",
        "looping",
        "cdn"
      ],
      "summary": "Сетка 10×5 точек пульсирует волной от центра: anime.js v4 считает задержку по расстоянию в grid-координатах, а не в порядке DOM.",
      "effect": "Каждая точка: scale 1→1.7 за 420ms outQuad, затем обратно за 900ms inOutQuad, бесконечный цикл. delay = stagger(70ms, {grid:[10,5], from:'center'}) — круглая волна расходится из середины сетки.",
      "when_to_use": "Hero-паттерны, фоны секций, визуализация данных и сеток; grid-stagger быстро даёт радиальную волну без ручных задержек.",
      "tech": [
        "animejs",
        "vanilla-js"
      ],
      "dependencies": [
        "anime.js 4.5 (CDN, UMD)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/animejs@4.5.0/dist/bundles/anime.umd.min.js\"></script>\n<section class=\"demo\">\n  <h2 class=\"demo__title\">Ripple grid</h2>\n  <div class=\"demo__grid\"></div>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  padding: 24px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__title { margin: 0 0 18px; font-size: 20px; }\n.demo__grid {\n  display: grid;\n  grid-template-columns: repeat(10, 1fr);\n  gap: 8px;\n}\n.demo__grid .dot {\n  aspect-ratio: 1;\n  border-radius: 50%;\n  background: #5b7cfa;\n  opacity: .85;\n}\n@media (prefers-reduced-motion: reduce) {\n  .demo__grid .dot { opacity: .5; }\n}",
        "js": "const { animate, stagger } = anime;\nconst grid = document.querySelector(\".demo__grid\");\nconst reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nfor (let i = 0; i < 50; i++) {\n  grid.insertAdjacentHTML(\"beforeend\", '<span class=\"dot\"></span>');\n}\n\nif (!reduced) {\n  animate(\".dot\", {\n    scale: [\n      { to: 1.7, duration: 420, ease: \"outQuad\" },\n      { to: 1, duration: 900, ease: \"inOutQuad\" }\n    ],\n    delay: stagger(70, { grid: [10, 5], from: \"center\" }),\n    loop: true\n  });\n}"
      },
      "source": {
        "name": "Anime.js Docs — Time staggering",
        "url": "https://animejs.com/documentation/stagger/time-staggering",
        "domain": "animejs.com"
      },
      "performance": "transform: scale (compositor-only) на 50 мелких узлах; бесконечный loop без пересоздания анимации. На сотнях точек проверьте FPS на слабых устройствах.",
      "a11y": "Гейт по matchMedia: при prefers-reduced-motion анимация не создаётся, точки статичны (CSS приглушает opacity).",
      "browser_support": "Chrome 90+, Safari 15+, Firefox 88+ (anime.js v4).",
      "ai_use": "For a radial ripple grid: build a 10×5 dot grid, then animate scale keyframes with delay stagger(70, {grid:[10,5], from:'center'}) and loop true.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2500,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-lenis-smooth-scroll",
      "name": "Lenis Smooth Scroll",
      "name_ru": "Lenis: инерционный скролл",
      "category": "technique",
      "tags": [
        "lenis",
        "smooth-scroll",
        "inertia",
        "cdn",
        "looping"
      ],
      "summary": "Lenis перехватывает скролл и интерполирует позицию с lerp 0.09; демо само ездит вверх-вниз, чтобы инерцию было видно в записи.",
      "effect": "lerp 0.09 мягко догоняет целевую позицию на каждом кадре (autoRaf). Каждые 2.6s lenis.scrollTo(низ/верх, duration 1.7s) — страница плавно доезжает и тормозит по экспоненциальной кривой догона.",
      "when_to_use": "Имиджевые лендинги, студии, сайты с параллаксом; синхронизируется с GSAP ScrollTrigger через lenis.on('scroll', ScrollTrigger.update).",
      "tech": [
        "lenis",
        "vanilla-js"
      ],
      "dependencies": [
        "Lenis 1.3.26 (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.min.js\"></script>\n<section class=\"demo\">\n  <h2 class=\"demo__title\">Smooth scroll</h2>\n  <div class=\"demo__card\">Lenis wraps native scroll, so position: sticky and anchors keep working.</div>\n  <div class=\"demo__card\">It syncs with GSAP ScrollTrigger via lenis.on('scroll', ScrollTrigger.update).</div>\n  <div class=\"demo__card\">Lerp ~0.1 gives the signature glide without breaking accessibility.</div>\n  <div class=\"demo__card\">Under prefers-reduced-motion smoothing is skipped and native scroll stays.</div>\n</section>",
        "css": ".demo {\n  min-height: 780px;\n  padding: 26px 22px 60px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__title { margin: 0 0 18px; font-size: 24px; }\n.demo__card {\n  margin-bottom: 14px;\n  padding: 20px;\n  border-radius: 14px;\n  background: #182031;\n  border: 1px solid #2a3246;\n  color: #c9d2e4;\n  font-size: 14px;\n  line-height: 1.5;\n}\n@media (prefers-reduced-motion: reduce) {\n  html { scroll-behavior: auto; }\n}",
        "js": "const reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nconst lenis = new Lenis({ autoRaf: true, lerp: reduced ? 1 : 0.09 });\n\n// demo loop: glide top / bottom so the inertia is visible in a recording\nif (!reduced) {\n  let forward = true;\n  setInterval(() => {\n    const max = document.documentElement.scrollHeight - innerHeight;\n    lenis.scrollTo(forward ? max : 0, { duration: 1.7 });\n    forward = !forward;\n  }, 2600);\n}"
      },
      "source": {
        "name": "Lenis — README (official docs)",
        "url": "https://github.com/darkroomengineering/lenis#readme",
        "domain": "github.com"
      },
      "performance": "Один rAF-цикл (autoRaf) и нативная прокрутка без transform-хаков, поэтому sticky и anchor работают. lerp=1 полностью отключает сглаживание.",
      "a11y": "При prefers-reduced-motion lerp=1 и цикл автоскролла не запускается — остаётся обычная нативная прокрутка; CSS-медиазапрос фиксирует scroll-behavior:auto.",
      "browser_support": "Chrome 90+, Safari 15.4+, Firefox 90+.",
      "ai_use": "Add Lenis smooth scroll: new Lenis({autoRaf:true, lerp:0.09}); pair with GSAP via lenis.on('scroll', ScrollTrigger.update) and disable smoothing under prefers-reduced-motion.",
      "preview": {
        "scroll": true,
        "mouse": null,
        "wait_ms": 3200,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-barba-page-transitions",
      "name": "Barba.js Page Transition",
      "name_ru": "Barba.js: переходы между страницами",
      "category": "technique",
      "tags": [
        "barba",
        "page-transition",
        "leave-enter",
        "web-animations",
        "cdn"
      ],
      "summary": "Barba перехватывает навигацию и отдаёт управление хукам leave/enter; демо прогоняет тот же leave→enter цикл между двумя макетами, так как у превью нет реальных переходов.",
      "effect": "Уход: 380ms cubic-bezier(.4,0,.2,1), opacity 1→0 и translateY 0→−22px. Приход: 480ms cubic-bezier(.2,.7,.2,1), из +26px и opacity 0. Каждые 2.4s макеты меняются местами; хуки возвращают Promise (animation.finished).",
      "when_to_use": "Многостраничные сайты, кейсы и разделы с переходами между страницами; для одностраничного лендинга не нужен.",
      "tech": [
        "barba",
        "vanilla-js",
        "web-animations"
      ],
      "dependencies": [
        "@barba/core 2.10.3 (CDN)"
      ],
      "difficulty": "medium",
      "code": {
        "html": "<script src=\"https://cdn.jsdelivr.net/npm/@barba/core@2.10.3/dist/barba.umd.js\"></script>\n<section class=\"demo\" data-barba=\"wrapper\">\n  <div class=\"demo__box\" data-barba=\"container\" data-barba-namespace=\"home\">\n    <div class=\"page is-active\">\n      <h2 class=\"demo__title\">Home</h2>\n      <p class=\"demo__sub\">Barba swaps the container, transitions handle the rest.</p>\n      <a class=\"demo__link\" href=\"./pricing.html\">Pricing →</a>\n    </div>\n    <div class=\"page\">\n      <h2 class=\"demo__title\">Pricing</h2>\n      <p class=\"demo__sub\">$29 per month, cancel anytime.</p>\n      <a class=\"demo__link\" href=\"./index.html\">← Back home</a>\n    </div>\n  </div>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  display: grid;\n  place-items: center;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__box {\n  width: min(360px, 90%);\n  padding: 24px;\n  border-radius: 18px;\n  background: #182031;\n  border: 1px solid #2a3246;\n  overflow: hidden;\n}\n.page { display: none; }\n.page.is-active { display: block; }\n.demo__title { margin: 0 0 8px; font-size: 24px; }\n.demo__sub { margin: 0 0 16px; color: #98a2b8; font-size: 14px; }\n.demo__link { color: #9fb0ff; text-decoration: none; font-size: 14px; }\n@media (prefers-reduced-motion: reduce) {\n  .page { opacity: 1; transform: none; }\n}",
        "js": "const reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nconst reset = (el) => el.getAnimations().forEach((a) => a.cancel());\n\nconst leave = ({ current }) => {\n  reset(current.container);\n  return current.container.animate(\n    [\n      { opacity: 1, transform: \"translateY(0)\" },\n      { opacity: 0, transform: \"translateY(-22px)\" }\n    ],\n    { duration: 380, easing: \"cubic-bezier(.4,0,.2,1)\", fill: \"forwards\" }\n  ).finished;\n};\n\nconst enter = ({ next }) => {\n  reset(next.container);\n  return next.container.animate(\n    [\n      { opacity: 0, transform: \"translateY(26px)\" },\n      { opacity: 1, transform: \"translateY(0)\" }\n    ],\n    { duration: 480, easing: \"cubic-bezier(.2,.7,.2,1)\", fill: \"forwards\" }\n  ).finished;\n};\n\nbarba.init({ transitions: [{ name: \"slide-fade\", leave, enter }] });\n\n// demo loop: drives the same hooks with fake page refs\nconst pages = [...document.querySelectorAll(\".page\")];\nlet i = 0;\nsetInterval(async () => {\n  const current = pages[i];\n  const next = pages[(i + 1) % pages.length];\n  if (!reduced) await leave({ current: { container: current } });\n  current.classList.remove(\"is-active\");\n  next.classList.add(\"is-active\");\n  if (reduced) {\n    reset(current);\n    reset(next);\n  } else {\n    enter({ next: { container: next } });\n  }\n  i = (i + 1) % pages.length;\n}, 2400);"
      },
      "source": {
        "name": "Barba.js Docs — Transitions",
        "url": "https://barba.js.org/docs/advanced/transitions/",
        "domain": "barba.js.org"
      },
      "performance": "WAAPI-переходы на transform/opacity; хуки возвращают animation.finished (Promise), поэтому Barba ждёт завершения анимации без таймеров.",
      "a11y": "При prefers-reduced-motion макеты переключаются мгновенно (хуки ничего не анимируют); CSS-медиазапрос фиксирует статичное состояние.",
      "browser_support": "Chrome 60+, Safari 13.1+, Firefox 60+ (Element.animate + Barba 2).",
      "ai_use": "For multi-page sites configure barba.init transitions with leave (fade+rise out 380ms) and enter (fade+rise in 480ms) hooks returning animation.finished; skip motion under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2400,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-auto-animate-list",
      "name": "AutoAnimate List Transitions",
      "name_ru": "AutoAnimate: add/remove в списке",
      "category": "technique",
      "tags": [
        "auto-animate",
        "list",
        "add-remove",
        "flip",
        "cdn"
      ],
      "summary": "Одна строка autoAnimate(list) — и добавление/удаление пунктов очереди анимируется само: новый пункт въезжает, остальные сдвигаются на новые позиции.",
      "effect": "Каждые 1.3s в конец списка добавляется пункт, а лишний (сверх 5) удаляется; AutoAnimate строит FLIP-переходы 320ms ease-in-out на WAAPI: соседи плавно переезжают, удалённый растворяется.",
      "when_to_use": "Динамические списки: очереди задач, уведомления, корзины, FAQ-аккордеоны. Разметку менять не нужно — только родительский элемент.",
      "tech": [
        "auto-animate",
        "vanilla-js"
      ],
      "dependencies": [
        "@formkit/auto-animate 0.10.0 (CDN, ESM — динамический import, UMD-сборки нет)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<section class=\"demo\">\n  <h2 class=\"demo__title\">Build queue</h2>\n  <ul class=\"demo__list\">\n    <li class=\"demo__item\">Discovery call</li>\n    <li class=\"demo__item\">Wireframes</li>\n    <li class=\"demo__item\">Design review</li>\n  </ul>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  padding: 26px 22px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__title { margin: 0 0 16px; font-size: 20px; }\n.demo__list {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n  display: grid;\n  gap: 10px;\n}\n.demo__item {\n  padding: 14px 16px;\n  border-radius: 12px;\n  background: #182031;\n  border: 1px solid #2a3246;\n  font-size: 14px;\n}\n.demo__item::before { content: \"◆ \"; color: #7d8cff; font-size: 10px; }\n@media (prefers-reduced-motion: reduce) {\n  .demo__item { transition: none !important; }\n}",
        "js": "const seeds = [\"Discovery call\", \"Wireframes\", \"Design review\", \"Prototype\", \"QA pass\"];\nconst reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nlet n = 3;\n\nimport(\"https://cdn.jsdelivr.net/npm/@formkit/auto-animate@0.10.0/index.min.js\").then(\n  ({ default: autoAnimate }) => {\n    const list = document.querySelector(\".demo__list\");\n\n    // the library respects reduced motion itself; skip setup entirely\n    if (!reduced) autoAnimate(list, { duration: 320, easing: \"ease-in-out\" });\n\n    setInterval(() => {\n      const li = document.createElement(\"li\");\n      li.className = \"demo__item\";\n      li.textContent = seeds[n % seeds.length] + \" · step \" + (n + 1);\n      list.appendChild(li);\n      n += 1;\n      if (list.children.length > 5) list.firstElementChild.remove();\n    }, 1300);\n  }\n);"
      },
      "source": {
        "name": "AutoAnimate — Official site/docs",
        "url": "https://auto-animate.formkit.com/",
        "domain": "auto-animate.formkit.com"
      },
      "performance": "FLIP-движок на WAAPI (transform), библиотека ~3 KB; анимируются только изменившиеся позиции, поэтому даже длинные списки остаются дешёвыми.",
      "a11y": "Библиотека сама уважает prefers-reduced-motion и отключает анимации; код дополнительно не включает её при reduced, CSS-медиазапрос подстраховывает.",
      "browser_support": "Chrome 80+, Safari 14+, Firefox 80+ (WAAPI + ESM dynamic import).",
      "ai_use": "For reorderable task/notification lists call autoAnimate(parentEl, {duration:320}) once; DOM add/remove then animates automatically — skip setup when prefers-reduced-motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    },
    {
      "id": "tech-swiper-coverflow",
      "name": "Swiper Coverflow Carousel",
      "name_ru": "Swiper: coverflow-карусель",
      "category": "technique",
      "tags": [
        "swiper",
        "carousel",
        "coverflow",
        "autoplay",
        "cdn"
      ],
      "summary": "Карусель Swiper с эффектом coverflow: центральный слайд крупнее и подсвечен, боковые уходят в depth с поворотом; автоплэй листает бесконечно.",
      "effect": "effect coverflow: rotate 18°, depth 130, modifier 1.4, slideShadows off; centeredSlides, slidesPerView 3, loop. Переход слайда 700ms, автоплэй 1300ms — карусель едет непрерывно.",
      "when_to_use": "Портфолио, отзывы, галереи кейсов, слайдер фич продукта; coverflow даёт объём без WebGL.",
      "tech": [
        "swiper",
        "vanilla-js"
      ],
      "dependencies": [
        "Swiper 14.2 (CDN: bundle CSS + JS)"
      ],
      "difficulty": "easy",
      "code": {
        "html": "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/swiper@14.2.0/swiper-bundle.min.css\">\n<script src=\"https://cdn.jsdelivr.net/npm/swiper@14.2.0/swiper-bundle.min.js\"></script>\n<section class=\"demo\">\n  <div class=\"swiper demo__swiper\">\n    <div class=\"swiper-wrapper\">\n      <div class=\"swiper-slide\">Discover</div>\n      <div class=\"swiper-slide\">Design</div>\n      <div class=\"swiper-slide\">Build</div>\n      <div class=\"swiper-slide\">Launch</div>\n      <div class=\"swiper-slide\">Scale</div>\n    </div>\n  </div>\n  <p class=\"demo__hint\">coverflow · centered · autoplay</p>\n</section>",
        "css": ".demo {\n  min-height: 320px;\n  display: grid;\n  align-content: center;\n  gap: 10px;\n  font-family: system-ui, sans-serif;\n  color: #f4f6fb;\n}\n.demo__swiper { width: 100%; padding: 24px 0; }\n.demo__swiper .swiper-slide {\n  height: 150px;\n  display: grid;\n  place-items: center;\n  border-radius: 16px;\n  font-size: 16px;\n  font-weight: 600;\n  color: #fff;\n  background: linear-gradient(150deg, #2b3a67, #161d31);\n  border: 1px solid #35406a;\n}\n.demo__swiper .swiper-slide-active {\n  background: linear-gradient(150deg, #5b7cfa, #b06ef5);\n}\n.demo__hint {\n  margin: 0;\n  text-align: center;\n  color: #98a2b8;\n  font-size: 12px;\n  letter-spacing: .08em;\n  text-transform: uppercase;\n}\n@media (prefers-reduced-motion: reduce) {\n  .demo__swiper .swiper-slide { transition: none; }\n}",
        "js": "const reduced = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nnew Swiper(\".demo__swiper\", {\n  effect: \"coverflow\",\n  slidesPerView: 3,\n  centeredSlides: true,\n  loop: true,\n  speed: reduced ? 0 : 700,\n  grabCursor: true,\n  autoplay: reduced ? false : { delay: 1300, disableOnInteraction: false },\n  coverflowEffect: {\n    rotate: 18,\n    stretch: 0,\n    depth: 130,\n    modifier: 1.4,\n    slideShadows: false\n  }\n});"
      },
      "source": {
        "name": "Swiper API — Coverflow Effect",
        "url": "https://swiperjs.com/swiper-api",
        "domain": "swiperjs.com"
      },
      "performance": "Эффект на CSS 3D transform (translate/rotate/scale) — compositor-friendly; loop и автоплэй работают на одном таймере, без клонирования узлов вручную.",
      "a11y": "При prefers-reduced-motion автоплэй выключен и speed:0 — слайды не двигаются сами; CSS-медиазапрос снимает transition.",
      "browser_support": "Chrome 90+, Safari 15+, Firefox 90+ (Swiper 14).",
      "ai_use": "For portfolio/review sliders init Swiper with effect:'coverflow', slidesPerView:3, centeredSlides, loop:true, coverflowEffect {rotate:18, depth:130, modifier:1.4} and autoplay disabled under reduced motion.",
      "preview": {
        "scroll": false,
        "mouse": null,
        "wait_ms": 2200,
        "bg": "#0b0d12"
      },
      "_file": "agent-08-libs.json"
    }
  ]
}