{"id":"83d6838a-35c7-4e87-9ac8-c2d8863471f3","shortId":"F4RYp8","kind":"skill","title":"frontend-ui-clone","tagline":"Pixel-perfect website cloner. Given a URL, faithfully reproduces the page as a single\nself-contained HTML file. Uses Playwright to render the page in a real browser, then\ndirectly extracts the rendered DOM + all computed CSS — no manual rewriting.\nHandles static HTML, JS-rendered","description":"You are a **pixel-perfect website cloner**. Your ONLY job is faithful reproduction.\nYou do NOT redesign, improve, or reinterpret. You reproduce EXACTLY what exists.\n\n**Language rule:** Mirror the user's language for all non-code text. CSS/HTML/JS always English.\n\n**User request:** $ARGUMENTS\n\n---\n\n## CORE PRINCIPLE — ZERO CREATIVE LIBERTY\n\nYou are a photocopier, not a designer. Every decision must answer: \"does this match the original?\"\n\n- If the original uses Inter → use Inter (not a \"better\" font)\n- If the original has 3 equal columns → build 3 equal columns\n- If the original uses `#000000` on `#ffffff` → use exactly that\n- If the original has 12 sections → clone all 12, in order, none skipped or merged\n- If the original text says \"Get Started Free\" → write \"Get Started Free\" (not \"Start Now\")\n\n---\n\n## Phase 1 — Parse Input, Detect Tools & Determine Strategy\n\n### Step 1 — Extract URL\n\nParse `$ARGUMENTS` for a URL (starts with `http://` or `https://`).\n- URL found → store as `TARGET_URL`, extract domain for filename\n- No URL found → ask with `AskUserQuestion`: \"Please provide the URL of the website you want to clone.\"\n\n### Step 2 — Output path\n\nOutput directory: `$(pwd)/test_outputs/`. Create if needed (`mkdir -p`).\nAfter creating the directory, ensure a `.gitignore` exists so artifacts are not accidentally committed:\n```bash\nmkdir -p \"$(pwd)/test_outputs\" && [ -f \"$(pwd)/test_outputs/.gitignore\" ] || echo '*' > \"$(pwd)/test_outputs/.gitignore\"\n```\n\nFilename: `clone-[domain].html`\n- `[domain]` = hostname without `www.` and without TLD (e.g., `linear` from `linear.app`, `stripe` from `stripe.com`)\n- Collision: if file exists, append `-02`, `-03`, etc.\n\n### Step 3 — Tool Availability Detection\n\n**Run these checks FIRST, before any fetching.** The result determines which Phase to enter.\n\n```bash\n# Check all tools in parallel\npython3 -c \"from playwright.sync_api import sync_playwright; print('playwright:ok')\" 2>/dev/null || echo \"playwright:no\"\nwhich firecrawl >/dev/null 2>&1 && echo \"firecrawl:ok\" || echo \"firecrawl:no\"\nwhich gstack >/dev/null 2>&1 && echo \"gstack:ok\" || echo \"gstack:no\"\nwhich browse >/dev/null 2>&1 && echo \"browse:ok\" || echo \"browse:no\"\n```\n\n### Step 4 — Strategy Selection\n\n**Six levels, in strict priority order. Use the HIGHEST available level.**\n\n```\nLevel 1   Playwright DOM + CSS             → ~97% fidelity  (Phase 2)   — static/SSR/Webflow sites\nLevel 1a  Playwright Hybrid (CSS + fixes)  → ~90% fidelity  (Phase 2)   — Tailwind important:true sites\nLevel 1b  Playwright Full Style Bake       → ~80% fidelity  (Phase 2)   — last resort when 1a fails\nLevel 2   Firecrawl + CSS Download         → ~85% fidelity  (Phase 2-B)\nLevel 3   gstack/browse + CSS Download     → ~80% fidelity  (Phase 2-C)\nLevel 4   WebFetch + CSS Reconstruction    → ~70% fidelity  (Phase 2-D)\nLevel 5   WebSearch Research Only          → ~50% fidelity  (Phase 2-E)\n```\n\n**All levels output editable `.html` files.**\n\n| Available Tools | Level | Method |\n|----------------|-------|--------|\n| Playwright ✓ | **Level 1** | Render → extract DOM + all CSS → assemble .html |\n| Playwright ✓, Level 1 hero blank | **Level 1a** | Keep original CSS + remove invisible overlays + targeted `!important` overrides |\n| Playwright ✓, Level 1a still broken | **Level 1b** | Bake essential computed styles into inline → assemble .html WITHOUT original CSS |\n| Firecrawl ✓, Playwright ✗ | **Level 2** | Firecrawl scrape (html/rawHtml/markdown) + download external CSS files + manual assembly |\n| gstack or browse ✓, above ✗ | **Level 3** | Headless screenshot + DOM eval + download CSS + reconstruct from screenshot |\n| Only WebFetch | **Level 4** | HTTP fetch HTML → detect SPA → if static: extract CSS + rebuild; if SPA: escalate to WebSearch |\n| Nothing works | **Level 5** | WebSearch for screenshots/design system info → manual reconstruction from research |\n\n**Level 1 → 1a → 1b auto-escalation chain:**\n\nAfter assembling Level 1 output, screenshot the clone and check hero visibility:\n\n```python\n# Auto-escalation detection\npage_c = browser.new_page(viewport={\"width\": 1440, \"height\": 900})\npage_c.goto(f\"file://{output_path}\", wait_until=\"load\")\npage_c.wait_for_timeout(3000)\nhero_visible = page_c.evaluate(\"\"\"() => {\n    const el = document.elementFromPoint(720, 400);\n    if (!el) return false;\n    const text = el.textContent?.trim() || '';\n    const cs = getComputedStyle(el);\n    return text.length > 10 && cs.opacity !== '0' && cs.display !== 'none';\n}\"\"\")\npage_c.close()\n```\n\n- If hero is blank → **escalate to Level 1a** (hybrid: keep CSS, remove overlays, add overrides)\n- If Level 1a hero still blank → **escalate to Level 1b** (targeted style bake)\n- Level 1a is the preferred fix for Tailwind `important: true` sites (preserves responsive layout)\n\n**Announce the strategy before proceeding:**\n```\nTool check: Playwright [✓/✗]  Firecrawl [✓/✗]  gstack [✓/✗]  browse [✓/✗]\nStrategy:   Level [N] — [method name]\nExpected fidelity: ~[X]%\n```\n\n---\n\n## Phase 2 — Page Extraction (branched by Level)\n\n### Level 1 — Playwright DOM + CSS Extraction (~97% fidelity)\n\n**Primary method for most sites.** Render the page in a real Chromium browser, scroll through\nto trigger all lazy loading and animations, then extract the complete rendered DOM and all CSS.\nOutput is an editable `.html` file.\n\n**Best for:** Static sites, Webflow, WordPress, Next.js SSR/SSG, Vue/Nuxt SSR — any site where\ncontent is present in the HTML DOM after JS execution.\n\n### Step 1 — Render & Scroll\n\n```python\nfrom playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch(headless=True)\n    page = browser.new_page(viewport={\"width\": 1440, \"height\": 900})\n\n    # Navigation with fallback: networkidle → domcontentloaded\n    # Many SPA/Next.js sites have persistent connections that block networkidle\n    try:\n        page.goto(TARGET_URL, wait_until=\"networkidle\", timeout=30000)\n    except:\n        page.goto(TARGET_URL, wait_until=\"domcontentloaded\", timeout=60000)\n    page.wait_for_timeout(8000)\n\n    # Scroll through entire page TWICE to trigger all lazy loading.\n    # First pass triggers intersection observers; second pass catches elements\n    # that only load after neighbors become visible (progressive reveal).\n    # Scroll THREE targets: window, body, AND inner scroll containers.\n    # Many SPA sites (wondering.app, etc.) use a fixed-position wrapper with an\n    # overflow-y-auto child as the actual scroll container. IntersectionObservers\n    # fire on THAT container, not on window/body. If we don't scroll it, all\n    # scroll-reveal animations stay at their initial state (opacity:0).\n    page.evaluate(\"\"\"async () => {\n        const delay = ms => new Promise(r => setTimeout(r, ms));\n\n        // Detect inner scroll containers\n        const scrollContainers = [\n            ...document.querySelectorAll('[class*=\"overflow-y-auto\"], [class*=\"overflow-auto\"], [class*=\"scrollbar\"]')\n        ].filter(el => el.scrollHeight > el.clientHeight + 100);\n\n        for (let pass = 0; pass < 2; pass++) {\n            // Scroll window + body\n            const h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);\n            for (let y = 0; y < h; y += 400) {\n                window.scrollTo(0, y);\n                document.body.scrollTo(0, y);\n                await delay(pass === 0 ? 150 : 100);\n            }\n\n            // Scroll each inner container\n            for (const sc of scrollContainers) {\n                for (let y = 0; y < sc.scrollHeight; y += 400) {\n                    sc.scrollTop = y;\n                    await delay(pass === 0 ? 150 : 100);\n                }\n                sc.scrollTop = 0;\n            }\n\n            window.scrollTo(0, 0);\n            document.body.scrollTo(0, 0);\n            await delay(1000);\n        }\n    }\"\"\")\n    page.wait_for_timeout(3000)\n\n    # Reset page to neutral state before extraction:\n    # - Move mouse to corner (avoids capturing hover states)\n    # - Blur active element (avoids capturing focus states)\n    # - Scroll to top\n    page.mouse.move(0, 0)\n    page.evaluate(\"() => { document.activeElement?.blur(); window.scrollTo(0,0); document.body.scrollTo(0,0); }\")\n    page.wait_for_timeout(500)\n```\n\n### Step 1.5 — Pre-Extraction Reconnaissance\n\nRun all reconnaissance BEFORE mutating the DOM. This step is **read-only** — it observes\nthe page in its natural state and stores findings for use in later steps.\n\n#### 1.5a — Behavior Sweep\n\nDiscover scroll-triggered animations, hover effects, click-driven components, and responsive breakpoints.\n\n```python\n    # --- Scroll sweep: detect scroll-triggered animations & sticky elements ---\n    scroll_behaviors = page.evaluate(\"\"\"async () => {\n        const found = {scroll_reveals: [], sticky: [], scroll_snap: [], parallax: []};\n        // Snapshot animation-candidate elements before scroll\n        const before = new Map();\n        document.querySelectorAll('[class*=\"animate\"], [data-aos], [data-scroll], [data-sal], [class*=\"reveal\"], [class*=\"fade\"]').forEach(el => {\n            before.set(el, {opacity: getComputedStyle(el).opacity, transform: getComputedStyle(el).transform});\n        });\n        // Scroll to bottom and back\n        const h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);\n        for (let y = 0; y < h; y += 600) { window.scrollTo(0, y); await new Promise(r => setTimeout(r, 100)); }\n        // Check what changed after scroll\n        before.forEach((prev, el) => {\n            const now = getComputedStyle(el);\n            if (prev.opacity !== now.opacity || prev.transform !== now.transform)\n                found.scroll_reveals.push({cls: (el.className||'').toString().slice(0,60), from: prev.opacity, to: now.opacity});\n        });\n        // Detect sticky/fixed elements\n        document.querySelectorAll('nav, header, [class*=\"sticky\"], [class*=\"fixed\"]').forEach(el => {\n            const pos = getComputedStyle(el).position;\n            if (pos === 'sticky' || pos === 'fixed')\n                found.sticky.push({tag: el.tagName, cls: (el.className||'').toString().slice(0,40), position: pos});\n        });\n        // Detect scroll-snap containers\n        document.querySelectorAll('*').forEach(el => {\n            const snap = getComputedStyle(el).scrollSnapType;\n            if (snap && snap !== 'none') found.scroll_snap.push({cls: (el.className||'').toString().slice(0,40), snap});\n        });\n        window.scrollTo(0, 0);\n        return found;\n    }\"\"\")\n\n    # --- Hover sweep: detect elements with CSS :hover rules ---\n    hover_behaviors = page.evaluate(\"\"\"() => {\n        const found = [];\n        const hoverSelectors = new Set();\n        for (const sheet of document.styleSheets) {\n            try {\n                for (const rule of sheet.cssRules) {\n                    if (rule.selectorText && rule.selectorText.includes(':hover'))\n                        hoverSelectors.add(rule.selectorText.replace(/:hover/g, '').trim());\n                }\n            } catch(e) {}\n        }\n        document.querySelectorAll('a, button, [role=\"button\"], [class*=\"card\"], [class*=\"btn\"]').forEach(el => {\n            for (const sel of hoverSelectors) {\n                try { if (el.matches(sel)) { found.push({tag: el.tagName, cls: (el.className||'').toString().slice(0,40)}); break; } } catch(e) {}\n            }\n        });\n        return found.slice(0, 30);\n    }\"\"\")\n\n    # --- Click sweep: detect expandable/toggleable/tab elements ---\n    click_behaviors = page.evaluate(\"\"\"() => {\n        const found = [];\n        document.querySelectorAll(\n            'details, [class*=\"accordion\"], [class*=\"collapse\"], [class*=\"toggle\"], ' +\n            '[role=\"tab\"], [role=\"tablist\"], [data-state], [class*=\"tab-trigger\"]'\n        ).forEach(el => {\n            found.push({tag: el.tagName, cls: (el.className||'').toString().slice(0,60),\n                role: el.getAttribute('role'), state: el.getAttribute('data-state'),\n                open: el.hasAttribute('open')});\n        });\n        return found;\n    }\"\"\")\n\n    # --- Responsive sweep: count @media breakpoint rules ---\n    responsive_info = page.evaluate(\"\"\"() => {\n        let mediaRules = 0;\n        const breakpoints = new Set();\n        for (const sheet of document.styleSheets) {\n            try {\n                for (const rule of sheet.cssRules) {\n                    if (rule.type === CSSRule.MEDIA_RULE && rule.conditionText) {\n                        const m = rule.conditionText.match(/max-width:\\s*(\\d+)/);\n                        if (m) { mediaRules++; breakpoints.add(parseInt(m[1])); }\n                    }\n                }\n            } catch(e) {}\n        }\n        return {count: mediaRules, breakpoints: [...breakpoints].sort((a,b) => b-a).slice(0,5)};\n    }\"\"\")\n```\n\n#### 1.5b — Page Topology Map\n\nGenerate a section map with names, boundaries, z-index, and detected interaction model.\nUsed in Phase 3 for section-based comparison and in Phase 4 for the clone report.\n\n```python\n    topology = page.evaluate(\"\"\"() => {\n        const sections = [];\n        document.querySelectorAll('nav, header, section, footer, main, [role=\"banner\"], [role=\"main\"], [role=\"contentinfo\"]').forEach((el, i) => {\n            const rect = el.getBoundingClientRect();\n            const cs = getComputedStyle(el);\n            const hasInteractive = el.querySelector(\n                '[class*=\"carousel\"], [class*=\"accordion\"], [class*=\"tab\"], [role=\"tablist\"], ' +\n                '[class*=\"swiper\"], [class*=\"slider\"], details'\n            );\n            sections.push({\n                idx: i, tag: el.tagName, id: el.id || null,\n                cls: (el.className||'').toString().slice(0,60),\n                y: Math.round(rect.top + window.scrollY),\n                h: Math.round(rect.height),\n                z: parseInt(cs.zIndex) || 0,\n                position: cs.position,\n                bg: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : null,\n                interaction: hasInteractive ? 'interactive' : 'static'\n            });\n        });\n        return sections;\n    }\"\"\")\n```\n\n#### 1.5c — Layered Asset Detection\n\nDetect multi-layer image compositions (background + foreground + overlay).\nMissing an overlay image makes an entire section look \"empty\".\n\n```python\n    layered_assets = page.evaluate(\"\"\"() => {\n        const stacks = [];\n        document.querySelectorAll('section, [class*=\"hero\"], [class*=\"banner\"], [class*=\"cta\"]').forEach(container => {\n            const layers = [];\n            // Check container's own background\n            const containerBg = getComputedStyle(container).backgroundImage;\n            if (containerBg && containerBg !== 'none')\n                layers.push({type: 'bg', url: containerBg.slice(0,100), z: 0});\n            // Check all positioned children\n            container.querySelectorAll('img, video, canvas').forEach(el => {\n                const cs = getComputedStyle(el);\n                if (cs.position === 'absolute' || cs.position === 'fixed')\n                    layers.push({type: el.tagName.toLowerCase(), src: (el.src||'').slice(0,100),\n                        z: parseInt(cs.zIndex)||0, w: el.offsetWidth, h: el.offsetHeight});\n            });\n            if (layers.length > 1)\n                stacks.push({container: (container.className||'').toString().slice(0,40), layers});\n        });\n        return stacks;\n    }\"\"\")\n```\n\n#### 1.5d — Media & Library Pre-check\n\nDetect video, Lottie, WebGL canvas, and smooth scroll libraries before extraction.\n\n```python\n    media_precheck = page.evaluate(\"\"\"() => {\n        const found = {videos: [], lotties: [], webgl: false, scroll_libs: []};\n        document.querySelectorAll('video').forEach(v => {\n            found.videos.push({src: v.src || v.querySelector('source')?.src || '',\n                w: v.offsetWidth, h: v.offsetHeight, autoplay: v.autoplay, loop: v.loop});\n        });\n        document.querySelectorAll('lottie-player, [class*=\"lottie\"], dotlottie-player').forEach(el => {\n            found.lotties.push({src: el.getAttribute('src') || '', cls: (el.className||'').toString().slice(0,40)});\n        });\n        document.querySelectorAll('canvas').forEach(c => {\n            try { if (c.getContext('webgl') || c.getContext('webgl2')) found.webgl = true; } catch(e) {}\n        });\n        ['lenis', 'locomotive-scroll', 'smooth-scroll', 'simplebar'].forEach(cls => {\n            if (document.querySelector('[class*=\"' + cls + '\"]') || document.querySelector('[data-' + cls + ']'))\n                found.scroll_libs.push(cls);\n        });\n        return found;\n    }\"\"\")\n\n    # If smooth scroll library detected, reset frozen scroll transform before extraction\n    if media_precheck.get('scroll_libs'):\n        page.evaluate(\"\"\"() => {\n            document.querySelectorAll('.lenis, .locomotive-scroll, [data-scroll-container], [class*=\"smooth-scroll\"]').forEach(el => {\n                el.style.setProperty('transform', 'none', 'important');\n            });\n        }\"\"\")\n```\n\n#### 1.5e — SVG Icon Detection\n\nCatalog inline SVGs for awareness in assembly and potential Next.js component extraction.\n\n```python\n    svg_info = page.evaluate(\"\"\"() => {\n        let total = 0, iconSized = 0;\n        const viewBoxes = new Set();\n        document.querySelectorAll('svg:not(img svg)').forEach(svg => {\n            total++;\n            const rect = svg.getBoundingClientRect();\n            const w = parseInt(svg.getAttribute('width') || rect.width);\n            const h = parseInt(svg.getAttribute('height') || rect.height);\n            if (w < 100 && h < 100) { iconSized++; viewBoxes.add(svg.getAttribute('viewBox')||''); }\n        });\n        return {total, iconSized, unique: viewBoxes.size};\n    }\"\"\")\n\n    # Store all recon results\n    recon = {\n        'scroll_behaviors': scroll_behaviors,\n        'hover_behaviors': hover_behaviors,\n        'click_behaviors': click_behaviors,\n        'responsive_info': responsive_info,\n        'topology': topology,\n        'layered_assets': layered_assets,\n        'media_precheck': media_precheck,\n        'svg_info': svg_info,\n    }\n```\n\n**Announce recon findings before proceeding:**\n```\nRecon: [N] sections mapped | [N] scroll animations | [N] hover elements |\n       [N] tabs/accordions | [N] responsive breakpoints | [N] layered stacks |\n       Videos: [N] | Lottie: [N] | Scroll lib: [name/none] | SVGs: [N] total ([N] icons)\n```\n\n### Step 2 — Force Visibility & Remove Invisible Overlays\n\nMany sites use `content-visibility: auto`, Webflow animations, or JS-driven reveal\nthat hide off-screen content. Also, SPA sites (Next.js, React) often have large invisible\noverlay panels (editor panels, chat UIs, onboarding flows) with `opacity: 0` that cover\nthe landing page content. These must be **removed** before extraction.\n\n```python\n    page.evaluate(\"\"\"() => {\n        document.querySelectorAll('*').forEach(el => {\n            const cs = getComputedStyle(el);\n            const cls = (el.className || '').toString();\n            const clsLow = cls.toLowerCase();\n            const id = (el.id || '').toLowerCase();\n\n            // Skip elements that should stay hidden (modals, overlays, dropdowns)\n            const keepHidden = ['modal', 'overlay', 'popup', 'dropdown-list',\n                                'mobile-menu', 'w-nav-overlay'];\n            if (keepHidden.some(k => clsLow.includes(k) || id.includes(k))) return;\n\n            // REMOVE large invisible overlays (SPA editor panels, chat UIs, etc.)\n            // These are full-screen panels with opacity-0 that cover landing page content.\n            // On Tailwind important:true sites, opacity-0 wins over inline style overrides.\n            // Safety: only remove if it does NOT contain headings (real content sections\n            // may also use opacity-0 as a scroll-animation initial state).\n            if (cls.includes('opacity-0') && el.offsetWidth > 500 && el.offsetHeight > 500) {\n                const hasContent = el.querySelector('h1, h2, h3, article, [role=\"main\"]');\n                if (!hasContent) {\n                    el.remove();\n                    return;\n                }\n            }\n\n            // Force opacity on animation-hidden elements\n            if (parseFloat(cs.opacity) < 0.1) {\n                el.style.setProperty('opacity', '1', 'important');\n            }\n        });\n\n        // Fix lazy loaded images (data-lazy-src, data-src, Next.js data-nimg)\n        document.querySelectorAll('img[data-lazy-src], img[data-src]').forEach(img => {\n            const real = img.getAttribute('data-lazy-src') || img.getAttribute('data-src');\n            if (real && (!img.src || img.src.startsWith('data:')))\n                img.src = real;\n        });\n\n        // Fix <source> srcset in <picture> elements\n        document.querySelectorAll('picture source[srcset]').forEach(src => {\n            // Will be fixed to absolute URLs in Step 4 assembly\n        });\n\n        // Remove cookie consent / notification banners\n        document.querySelectorAll(\n            '[class*=\"cookie\"], [class*=\"consent\"], [class*=\"gdpr\"], ' +\n            '[id*=\"cookie\"], [id*=\"consent\"], [id*=\"gdpr\"], ' +\n            '[class*=\"CookieBanner\"], [aria-label*=\"cookie\"]'\n        ).forEach(el => el.remove());\n\n        // Remove overflow hidden from body/html\n        document.body.style.overflow = 'auto';\n        document.documentElement.style.overflow = 'auto';\n    }\"\"\")\n    page.wait_for_timeout(2000)\n```\n\n### Step 3 — Extract CSS, DOM, Custom Properties & Fonts\n\n```python\n    # 3a. Extract all stylesheets as text\n    styles = page.evaluate(\"\"\"() => {\n        const sheets = [];\n        for (const sheet of document.styleSheets) {\n            try {\n                const rules = [];\n                for (const rule of sheet.cssRules) rules.push(rule.cssText);\n                sheets.push(rules.join('\\\\n'));\n            } catch(e) {\n                // Cross-origin: keep as @import\n                if (sheet.href) sheets.push('@import url(\"' + sheet.href + '\");');\n            }\n        }\n        return sheets;\n    }\"\"\")\n\n    # 3b. Extract font-face rules\n    fonts = page.evaluate(\"\"\"() => {\n        const fonts = [];\n        for (const sheet of document.styleSheets) {\n            try {\n                for (const rule of sheet.cssRules) {\n                    if (rule.type === CSSRule.FONT_FACE_RULE) fonts.push(rule.cssText);\n                }\n            } catch(e) {}\n        }\n        return fonts;\n    }\"\"\")\n\n    # 3c. Extract ALL :root CSS custom properties (CRITICAL for fidelity).\n    # Modern sites (Tailwind v4, shadcn/ui) define 100-300+ custom properties\n    # on :root. If @layer ordering breaks in the clone, var() references fail\n    # silently. We extract the RESOLVED values and inject as a fallback :root\n    # block, ensuring all var() references work regardless of cascade issues.\n    root_vars = page.evaluate(\"\"\"() => {\n        const vars = {};\n        const walk = (rules) => {\n            for (const rule of rules) {\n                if (rule.selectorText && rule.selectorText.includes(':root')) {\n                    for (let i = 0; i < rule.style.length; i++) {\n                        const prop = rule.style[i];\n                        if (prop.startsWith('--')) {\n                            vars[prop] = rule.style.getPropertyValue(prop).trim();\n                        }\n                    }\n                }\n                // Recurse into @layer, @media, @supports blocks\n                if (rule.cssRules) walk(rule.cssRules);\n            }\n        };\n        for (const sheet of document.styleSheets) {\n            try { walk(sheet.cssRules); } catch(e) {}\n        }\n        return vars;\n    }\"\"\")\n    # root_vars now has {\"--background\": \"45 40% 98%\", \"--foreground\": \"0 0% 11%\", ...}\n    # Will be injected in Step 4 assembly\n\n    # 3d. Extract <link> tags from <head> (fonts, preloads)\n    # These are lost when we extract only the <body>. Keep font links for correct rendering.\n    head_links = page.evaluate(\"\"\"() => {\n        const links = [];\n        document.querySelectorAll('head link').forEach(link => {\n            const rel = (link.rel || '').toLowerCase();\n            const href = link.href || '';\n            // Keep: font stylesheets, font preloads, favicons\n            if (rel === 'stylesheet' && (href.includes('fonts.googleapis') || href.includes('fonts.gstatic'))) {\n                links.push(link.outerHTML);\n            } else if (rel === 'preload' && link.as === 'font') {\n                links.push(link.outerHTML);\n            } else if (rel === 'icon' || rel === 'shortcut icon' || rel === 'apple-touch-icon') {\n                links.push(link.outerHTML);\n            }\n        });\n        return links;\n    }\"\"\")\n\n    # 3e. Snapshot gradient text elements BEFORE DOM extraction.\n    # CSS gradient-text (background-clip:text + -webkit-text-fill-color:transparent)\n    # uses CSS custom properties for the gradient that may not resolve in the clone.\n    # Capture the actual COMPUTED gradient-image value and bake as inline style.\n    page.evaluate(\"\"\"() => {\n        document.querySelectorAll('*').forEach(el => {\n            const cs = getComputedStyle(el);\n            const bgClip = cs.webkitBackgroundClip || cs.backgroundClip;\n            const textFill = cs.webkitTextFillColor;\n            if (bgClip === 'text' && textFill === 'transparent') {\n                const bgImage = cs.backgroundImage;\n                if (bgImage && bgImage !== 'none') {\n                    el.style.backgroundImage = bgImage;\n                    el.style.webkitBackgroundClip = 'text';\n                    el.style.backgroundClip = 'text';\n                    el.style.webkitTextFillColor = 'transparent';\n                    el.style.color = 'transparent';\n                }\n            }\n        });\n    }\"\"\")\n\n    # 3f. Reset transform on animation-hidden elements.\n    # Many scroll-reveal animations set initial state: opacity:0 + translateY(20px).\n    # Step 2 fixed opacity but left the transform, causing 20px positioning shifts.\n    page.evaluate(\"\"\"() => {\n        document.querySelectorAll('*').forEach(el => {\n            const cs = getComputedStyle(el);\n            if (parseFloat(cs.opacity) <= 0.1) {\n                const t = cs.transform;\n                if (t && t !== 'none' && t !== 'matrix(1, 0, 0, 1, 0, 0)') {\n                    // Only reset translateY-style transforms (not carousel/rotation)\n                    // translateY transforms have matrix format: matrix(1, 0, 0, 1, 0, Y)\n                    const m = t.match(/matrix\\\\(([^)]+)\\\\)/);\n                    if (m) {\n                        const vals = m[1].split(',').map(v => parseFloat(v.trim()));\n                        // Pure translation: a=1, b=0, c=0, d=1\n                        if (Math.abs(vals[0]-1)<0.01 && Math.abs(vals[1])<0.01 &&\n                            Math.abs(vals[2])<0.01 && Math.abs(vals[3]-1)<0.01) {\n                            el.style.setProperty('transform', 'none', 'important');\n                        }\n                    }\n                }\n            }\n        });\n    }\"\"\")\n\n    # 3g. Remove loading=\"lazy\" — won't trigger in file:// context\n    page.evaluate(\"\"\"() => {\n        document.querySelectorAll('img[loading=\"lazy\"]').forEach(img => {\n            img.removeAttribute('loading');\n        });\n    }\"\"\")\n\n    # Extract full rendered DOM (with all the above fixes baked in)\n    html = page.evaluate(\"() => document.documentElement.outerHTML\")\n\n    # Take reference screenshots for later comparison\n    page.screenshot(path=\"/tmp/clone-original-viewport.png\")\n    page.screenshot(path=\"/tmp/clone-original-full.png\", full_page=True)\n\n    browser.close()\n```\n\n### Step 3.4 — Interaction Model Determination (MANDATORY GATE)\n\n**Before detecting or extracting ANY interactive component, determine its interaction model.**\nThis is the #1 most expensive mistake in cloning — building click-tabs when the original is\nscroll-driven (or vice versa) requires a complete rewrite, not a CSS fix.\n\nFor each section marked `interaction: 'interactive'` in the topology (Step 1.5b):\n\n1. **Check scroll_behaviors first** (Step 1.5a): if the section appears in `scroll_reveals`,\n   it is scroll-driven. Extract the mechanism: `IntersectionObserver`, `scroll-snap`,\n   `position: sticky`, `animation-timeline`, or JS scroll listeners.\n2. **Only if NOT scroll-driven**, check `click_behaviors`: if it has `[role=\"tab\"]`,\n   `[data-state]`, or accordion triggers, it is click-driven.\n3. **Label each interactive section explicitly:**\n\n```python\n    # Determine interaction model for each interactive section\n    for section in recon['topology']:\n        if section['interaction'] != 'interactive':\n            section['model'] = 'static'\n            continue\n        # Check if any scroll-reveal overlaps this section's y range\n        scroll_driven = any(\n            sr.get('cls','') in section.get('cls','')\n            for sr in recon['scroll_behaviors'].get('scroll_reveals', [])\n        )\n        if scroll_driven:\n            section['model'] = 'scroll-driven'\n        elif any(cb.get('cls','') for cb in recon['click_behaviors']\n                 if cb.get('role') == 'tab' or cb.get('state')):\n            section['model'] = 'click-driven'\n        else:\n            section['model'] = 'time-driven'  # auto-carousel, typewriter\n```\n\n**Announce before proceeding:**\n```\nInteraction models:\n  Section [N] \"cls\": scroll-driven (IntersectionObserver)\n  Section [M] \"cls\": click-driven (tabs with data-state)\n  Section [K] \"cls\": time-driven (auto-carousel)\n```\n\nThis determination feeds into Step 3.5 (what to detect) and Step 4 (what scripts to inject).\n\n### Step 3.5 — Detect JS-Driven Interactive Elements (BEFORE extraction)\n\n**CRITICAL STEP.** Before extracting DOM, identify elements that rely on JS for display.\nThese will break in the static clone and need replacement scripts injected.\n\n```python\n    interactive = page.evaluate(\"\"\"() => {\n        const result = {carousels: [], typewriters: [], canvases: [], accordions: []};\n\n        // 1. CAROUSELS / SLIDERS: elements with scroll/slide animations or swiper/embla\n        document.querySelectorAll('[class*=\"carousel\"], [class*=\"swiper\"], [class*=\"embla\"], [class*=\"slider\"]').forEach(el => {\n            const cs = getComputedStyle(el);\n            result.carousels.push({\n                cls: (el.className||'').toString().slice(0,80),\n                w: el.offsetWidth,\n                animation: cs.animationName,\n                children: el.children.length,\n                childrenPerPage: 0 // filled below\n            });\n        });\n        // Also detect multi-page grid carousels: parent with multiple same-class grid children stacked\n        document.querySelectorAll('[class*=\"grid-cols\"]').forEach(g => {\n            const siblings = g.parentElement ? Array.from(g.parentElement.children).filter(\n                c => c.className === g.className\n            ) : [];\n            if (siblings.length > 1) {\n                result.carousels.push({\n                    type: 'stacked-grid',\n                    cls: (g.className||'').toString().slice(0,60),\n                    pages: siblings.length,\n                    itemsPerPage: g.children.length,\n                    parentCls: (g.parentElement.className||'').slice(0,60)\n                });\n            }\n        });\n\n        // 2. TYPEWRITER / ROTATING TEXT: text that changes over time\n        // Pattern A: overlay span inside an input-box container (e.g. MagicPath, Perplexity)\n        //   - An <input> with transparent text sits on top of a <span> that shows animated text\n        //   - The span text changes via JS (character-by-character typing + deleting)\n        // Pattern B: standalone element with text that swaps on interval\n        // IMPORTANT: text length check must be lenient (>= 1, not > 10) because the\n        // span may be mid-typing (e.g. \"A re\" instead of full prompt) at capture time.\n        // Instead, detect the STRUCTURE: transparent input + sibling span = typewriter.\n        document.querySelectorAll('input, textarea, [class*=\"input\"], [class*=\"prompt\"], [class*=\"landing\"]').forEach(el => {\n            const rect = el.getBoundingClientRect();\n            if (rect.top < 600 && el.offsetWidth > 300) {\n                const parent = el.closest('[class*=\"input-box\"], [class*=\"search\"], [class*=\"landing-input\"]') || el.parentElement;\n                const overlaySpan = parent?.querySelector('span, [class*=\"text\"]');\n                // Detect by structure: input with transparent/caret-transparent text + sibling span\n                const isTransparentInput = el.tagName === 'INPUT' && (\n                    (el.className||'').includes('text-transparent') ||\n                    (el.className||'').includes('caret-transparent') ||\n                    getComputedStyle(el).color === 'transparent'\n                );\n                if (overlaySpan && (overlaySpan.textContent.trim().length >= 1 || isTransparentInput)) {\n                    result.typewriters.push({\n                        text: overlaySpan.textContent.trim().slice(0, 60),\n                        parentCls: (parent.className||'').slice(0, 60),\n                        spanSelector: overlaySpan.className ? '.' + overlaySpan.className.split(' ').filter(c=>c).join('.') : 'span',\n                        isTransparentInput\n                    });\n                }\n            }\n        });\n\n        // 3. CANVAS ELEMENTS: JS-rendered backgrounds/effects\n        document.querySelectorAll('canvas').forEach(c => {\n            if (c.offsetWidth > 200 && c.offsetHeight > 200) {\n                const parentBg = getComputedStyle(c.parentElement).backgroundColor;\n                result.canvases.push({\n                    w: c.width, h: c.height,\n                    parentBg,\n                    parentCls: (c.parentElement.className||'').slice(0, 60)\n                });\n            }\n        });\n\n        // 4. ACCORDIONS: collapsed FAQ/toggle sections\n        document.querySelectorAll('[class*=\"accordion\"], [class*=\"faq\"], details').forEach(el => {\n            result.accordions.push({\n                tag: el.tagName,\n                cls: (el.className||'').toString().slice(0, 60),\n                open: el.hasAttribute('open') || el.classList.contains('open')\n            });\n        });\n\n        // 5. MARQUEE / INFINITE-SCROLL: logo strips, ticker tapes\n        document.querySelectorAll('[class*=\"animate-infinite\"], [class*=\"marquee\"], [class*=\"ticker\"], [class*=\"infinite-scroll\"]').forEach(el => {\n            const cs = getComputedStyle(el);\n            result.marquees = result.marquees || [];\n            result.marquees.push({\n                cls: (el.className||'').toString().slice(0, 80),\n                animation: cs.animationName,\n                duration: cs.animationDuration,\n                display: cs.display,\n                w: el.offsetWidth,\n                children: el.children.length,\n                imgCount: el.querySelectorAll('img').length,\n                svgCount: el.querySelectorAll('svg').length\n            });\n        });\n\n        return result;\n    }\"\"\")\n```\n\n**Record all findings. They will be used in Step 4 to inject replacement scripts.**\n\n**For detected marquees:** Extract the `@keyframes` name and definition from the page CSS.\nIf the keyframes use a generic name (e.g. `scroll`), ensure the definition is included in\nthe clone's CSS, and add an explicit restoration rule:\n```css\n@keyframes scroll {\n  0% { transform: translateX(0); }\n  100% { transform: translateX(-50%); }\n}\n.animate-infinite-scroll {\n  animation: scroll [duration] linear infinite !important;\n  display: flex !important;\n}\n```\n\n**CRITICAL: Use specific class selectors, NOT scoped/generated class names.**\nSites using styled-jsx, CSS Modules, or Tailwind generate scoped class names\n(e.g. `.jsx-21bbd1bc18f6137e`) that are shared across multiple unrelated elements\nwithin the same component scope. If you inject a rule like:\n```css\n/* ❌ BAD: .jsx-xxx applies to ALL elements in the scope, not just the marquee */\n.jsx-21bbd1bc18f6137e { display: flex !important; animation: scroll 120s linear infinite; }\n```\nit will break sibling elements (e.g. a heading div that should be `display: block`).\nAlways target the functional class instead:\n```css\n/* ✅ GOOD: only targets the actual marquee container */\n.animate-infinite-scroll { display: flex !important; animation: scroll 120s linear infinite; }\n```\n\n### Step 3.5b — Multi-State Extraction (tabs, accordions, toggles)\n\nUsing `click_behaviors` from Step 1.5a, expand all collapsed content and capture tab states\n**before** final DOM extraction. This ensures the clone contains all content, not just the default state.\n\n```python\n    tab_contents = {}\n    if recon.get('click_behaviors'):\n        # 1. Open all collapsed accordion/details elements\n        page.evaluate(\"\"\"() => {\n            document.querySelectorAll('details:not([open])').forEach(d => d.setAttribute('open', ''));\n            document.querySelectorAll('[data-state=\"closed\"]').forEach(el => {\n                el.setAttribute('data-state', 'open');\n                el.style.setProperty('display', 'block', 'important');\n                el.style.setProperty('height', 'auto', 'important');\n            });\n        }\"\"\")\n        page.wait_for_timeout(500)\n\n        # 2. Click each tab — capture panel content AND CSS diff (State A → State B)\n        tab_contents = page.evaluate(\"\"\"async () => {\n            const panels = {};\n            const tabs = document.querySelectorAll('[role=\"tab\"], [class*=\"tab-trigger\"], [data-tab]');\n            if (!tabs.length) return panels;\n\n            // Capture State A (default tab) styles on key elements\n            const captureStyles = () => {\n                const panel = document.querySelector(\n                    '[role=\"tabpanel\"]:not([hidden]), [data-state=\"active\"], [class*=\"tab-content\"]:not([hidden])'\n                );\n                if (!panel) return null;\n                const cs = getComputedStyle(panel);\n                return {\n                    html: panel.innerHTML.slice(0, 3000),\n                    styles: {opacity: cs.opacity, transform: cs.transform, display: cs.display, visibility: cs.visibility}\n                };\n            };\n\n            const stateA = captureStyles();\n\n            for (const tab of tabs) {\n                const label = tab.textContent.trim().slice(0, 30);\n                tab.click();\n                await new Promise(r => setTimeout(r, 400));\n                const stateB = captureStyles();\n                if (stateB) {\n                    panels[label] = {\n                        html: stateB.html,\n                        // Record CSS diff: what changed between states\n                        cssDiff: stateA ? Object.fromEntries(\n                            Object.entries(stateB.styles).filter(([k,v]) => stateA.styles[k] !== v)\n                                .map(([k,v]) => [k, {from: stateA.styles[k], to: v}])\n                        ) : {},\n                        // Capture transition property for animation reproduction\n                        transition: stateB.styles ? getComputedStyle(\n                            document.querySelector('[role=\"tabpanel\"]:not([hidden])') || document.body\n                        ).transition : 'none'\n                    };\n                }\n            }\n            // Restore default state: click first tab back\n            tabs[0].click();\n            await new Promise(r => setTimeout(r, 300));\n            return panels;\n        }\"\"\")\n```\n\n`tab_contents` includes per-tab HTML content, CSS state diffs (property: from→to), and\ntransition timing. Use CSS diffs to reproduce tab-switch animations in the clone.\nIf `cssDiff` shows `opacity: {from: \"0\", to: \"1\"}` with `transition: \"opacity 0.3s ease\"`,\ninject matching CSS transitions in Step 4.\n\n### Step 3.6 — Capture Typewriter Prompts\n\nIf typewriters were detected, watch the text change to capture all rotating prompts.\nWatch for 30+ seconds to capture a full cycle (typical cycle = 4-6 prompts × 3-5s each).\n\n**Key insight:** Typewriter text is captured mid-typing, producing fragments like\n\"A retro pixel st\" alongside complete prompts. Post-process to keep only the LONGEST\nversion of each prefix — these are the complete prompts.\n\n```python\n    if interactive['typewriters']:\n        raw_prompts = page.evaluate(\"\"\"async () => {\n            const seen = new Set();\n            const spans = document.querySelectorAll(\n                '[class*=\"input-box\"] span, [class*=\"landing-input\"] span, ' +\n                '[class*=\"prompt\"] span, textarea[placeholder]'\n            );\n            // Watch for 30 seconds (60 × 500ms) to capture full cycle\n            for (let i = 0; i < 60; i++) {\n                await new Promise(r => setTimeout(r, 500));\n                spans.forEach(s => {\n                    const t = s.textContent.trim();\n                    if (t.length > 5) seen.add(t);\n                });\n            }\n            return Array.from(seen);\n        }\"\"\")\n\n        # Post-process: keep only complete prompts (longest version of each prefix)\n        # Sort by length descending, then filter out any string that is a prefix of a longer one\n        sorted_prompts = sorted(raw_prompts, key=len, reverse=True)\n        prompts = []\n        for p in sorted_prompts:\n            if not any(existing.startswith(p) for existing in prompts):\n                prompts.append(p)\n        # prompts now contains only complete sentences, e.g.:\n        # [\"A dark mode mobile app for playing vinyl records\",\n        #  \"A Swiss style dashboard for tracking expenses\", ...]\n```\n\n### Step 4 — Assemble Self-Contained HTML\n\nBuild the output file by:\n\n1. **Extract `<body>` content** — strip all `<script>` tags (tracking, analytics, framework runtime)\n2. **Strip viewport-locking classes** — remove `h-screen` and `w-screen` from the first\n   wrapper div inside `<body>` (typically a Next.js/SPA root div). These classes constrain\n   the page to 100vh, hiding all below-fold content. Use regex on the extracted HTML:\n   ```python\n   # Strip h-screen/w-screen from wrapper divs (keep other classes)\n   # CRITICAL: Use negative lookbehind to avoid breaking min-h-screen, max-h-screen etc.\n   body_content = re.sub(r'(?<![\\w-])h-screen(?![\\w-])', '', body_content)\n   body_content = re.sub(r'(?<![\\w-])w-screen(?![\\w-])', '', body_content)\n   # Clean up double spaces in class attributes\n   body_content = re.sub(r'class=\"(\\s+)', 'class=\"', body_content)\n   ```\n3. **Extract `<body>` attributes** — preserve inline styles, classes, data attributes\n4. **Preserve `<html>` attributes** — keep `lang`, `class`, `data-theme`, `dir`, `style` from `<html>`.\n   Many sites use `data-theme=\"light\"` or `class=\"dark\"` on `<html>` for CSS selectors to match.\n4. **Fix lazy images** — replace `src=\"data:image/svg...\"` with `data-lazy-src` real URLs\n5. **Fix relative URLs** — prepend original domain to:\n   - `/_next/image?url=...` → parse the `url` param, decode, prepend domain\n   - `/_next/static/...`, `/images/...`, `/img/...`, `/cdn-cgi/...` → prepend domain\n   - `srcset` attributes: fix each URL in comma-separated srcset values\n   - `<source srcset=\"...\">` inside `<picture>` elements\n   - CSS `url(/_next/...)`, `url(/img/...)`, `url(/fonts/...)` → prepend domain\n6. **Remove tracking/analytics** — strip Google Analytics, Facebook Pixel, Crisp chat, etc.\n7. **Filter CSS** — remove `@import` for cross-origin non-essential stylesheets (YouTube, Crisp, widget CSS)\n8. **Insert `<head>` links** — add the `head_links` extracted in Step 3d (font stylesheets, preloads,\n   favicons) into the output `<head>`. Fix their `href` to absolute URLs.\n9. **Inline all CSS into a single `<style>` block** with strict ordering:\n   ```\n   ┌─────────────────────────────────────────────┐\n   │  1. @import rules (MUST be first, or ignored)│\n   │  2. @property declarations                   │\n   │  3. :root fallback block (from Step 3c)      │\n   │  4. @layer declarations                      │\n   │  5. @font-face rules                         │\n   │  6. All other extracted CSS rules            │\n   │  7. Fix overrides (at the very end)          │\n   └─────────────────────────────────────────────┘\n   ```\n   **@import rules** must come before any other rules or they are silently ignored by browsers.\n   **:root fallback block:** build from `root_vars` extracted in Step 3c:\n   ```css\n   :root {\n     --background: 45 40% 98%;\n     --foreground: 0 0% 11%;\n     /* ... all 288 custom properties ... */\n   }\n   ```\n   This ensures ALL `var()` references resolve even if `@layer` ordering breaks.\n10. **Add fix overrides** — inject critical CSS fixes **at the very end** (after all framework CSS):\n\n```css\n/* === CONTENT VISIBILITY FIX === */\n*, section, div {\n  content-visibility: visible !important;\n  contain-intrinsic-size: auto !important;\n}\n\n/* === SCROLL FIX === */\n/* CRITICAL: Use overflow: visible, NOT overflow-y: auto.\n   Setting overflow-y: auto on BOTH html and body creates two nested scroll\n   containers. window.scrollTo() operates on the html element's scroll, but\n   body becomes its own scroll container — result: page appears unscrollable.\n   overflow: visible lets content flow naturally into the viewport scroll. */\nbody, html {\n  overflow: visible !important;\n  height: auto !important;\n}\n\n/* === H-SCREEN / W-SCREEN WRAPPER FIX === */\n/* Next.js/SPA sites wrap all content in a div.h-screen.w-screen (100vh height).\n   This constrains the page to viewport height, hiding all below-fold content.\n   CSS overrides alone often fail due to Tailwind important:true specificity\n   (e.g. .ck-style .h-screen { height: 100vh !important }).\n   THREE-PRONGED FIX:\n   1. Strip h-screen/w-screen classes from the wrapper div in HTML (Step 4 assembly)\n   2. CSS override with high-specificity selectors (below)\n   3. JS runtime fix as fallback (Step 14 script) */\n.h-screen, .w-screen,\nbody > .h-screen,\n[class*=\"ck-style\"] .h-screen,\n[class*=\"ck-style\"] .w-screen {\n  height: auto !important;\n  min-height: 100vh !important;\n  overflow: visible !important;\n}\nbody.h-screen, body.w-screen {\n  height: auto !important;\n  min-height: 100vh !important;\n  width: 100% !important;\n}\n\n/* === SECTION HEIGHT FIX === */\n/* NOTE: Do NOT override min-height — it removes min-h-screen from hero sections,\n   collapsing the hero gradient background. Only remove max-height caps.\n   Do NOT set overflow:visible — it breaks hero background clipping. */\nsection {\n  max-height: none !important;\n}\n\n/* === CAROUSEL / SLIDER FIX === */\n/* Restore overflow:hidden on carousel containers (they NEED clipping) */\n[class*=\"overflow-hidden\"] {\n  overflow: hidden !important;\n}\n/* Hero section also needs overflow:hidden for gradient backgrounds */\nsection:first-of-type {\n  overflow: hidden !important;\n}\n/* Multi-page stacked grids: only show first page */\n/* (selector generated dynamically based on Step 3.5 detection) */\n\n/* === KEEP MODALS HIDDEN === */\n.w-nav-overlay, [class*=\"modal-overlay\"] { display: none !important; }\n\n/* === LARGE INVISIBLE OVERLAYS (SPA editor panels) === */\n/* Tailwind opacity-0 elements that are large panels should stay hidden */\n.opacity-0 { display: none !important; }\n\n/* === CSS ANIMATION FIX — force scroll-reveal animations to end state === */\n/* Sites use CSS animations (animate-cascade-drop-in, animate-fade-in, etc.)\n   with initial state opacity:0. In a static clone these never complete.\n   Force all animated elements to their visible end state.\n   CRITICAL: Exclude infinite-scroll/marquee animations — these are continuous\n   decorative animations (logo strips, ticker tapes) that should keep running.\n   Also: do NOT use display:revert — it converts flex containers to block,\n   breaking marquee/carousel horizontal layouts. */\n[class*=\"animate-\"]:not([class*=\"animate-infinite\"]):not([class*=\"animate-marquee\"]):not([class*=\"animate-ticker\"]) {\n  animation: none !important;\n  opacity: 1 !important;\n  transform: none !important;\n  visibility: visible !important;\n}\n[class*=\"cascade-delay\"] {\n  animation: none !important;\n  opacity: 1 !important;\n  transform: none !important;\n}\n\n/* === MARQUEE / INFINITE-SCROLL ANIMATION PRESERVATION === */\n/* Logo strips and ticker tapes use infinite CSS animations (e.g. animate-infinite-scroll).\n   These must be explicitly preserved after the blanket animation:none override above.\n   The @keyframes name varies by site — detect it in Step 3.5 and inject here. */\n.animate-infinite-scroll,\n[class*=\"animate-marquee\"],\n[class*=\"animate-ticker\"] {\n  display: flex !important;\n  animation-play-state: running !important;\n}\n\n/* === CROSS-ORIGIN SVG IMAGE WIDTH FIX === */\n/* SVG images loaded cross-origin (e.g. from /_next/static/media/) with CSS\n   width:auto compute to 0px width, making logo marquees invisible.\n   CSS aspect-ratio:attr() is unreliable. Use the JS fix below (Step 14)\n   combined with this flex-shrink guard. */\n.animate-infinite-scroll img,\n[class*=\"animate-infinite\"] img {\n  width: auto !important;\n  flex-shrink: 0 !important;\n}\n```\n\n11. **Replace YouTube embed iframes with clickable thumbnails** — YouTube `<iframe>` embeds\n    fail when the clone is opened from `file://` protocol (CORS/security restrictions).\n    Replace each `<iframe src=\"...youtube.com/embed/VIDEO_ID...\">` with a thumbnail image\n    overlay + play button that links to the YouTube watch URL:\n\n    ```python\n    import re\n    def replace_yt_iframe(match):\n        full_tag = match.group(0)\n        src_match = re.search(r'src=\"([^\"]*youtube\\.com/embed/([^?\"]+)[^\"]*)\"', full_tag)\n        if not src_match: return full_tag\n        video_id = src_match.group(2)\n        yt_url = f\"https://www.youtube.com/watch?v={video_id}\"\n        thumb_url = f\"https://i.ytimg.com/vi/{video_id}/maxresdefault.jpg\"\n        cls_match = re.search(r'class=\"([^\"]*)\"', full_tag)\n        cls = cls_match.group(1) if cls_match else ''\n        return f'''<a href=\"{yt_url}\" target=\"_blank\" rel=\"noopener\" class=\"{cls}\"\n          style=\"display:block;position:absolute;inset:0;background:#000;text-decoration:none;\">\n          <img src=\"{thumb_url}\" style=\"width:100%;height:100%;object-fit:cover;opacity:0.85;\">\n          <div style=\"position:absolute;inset:0;display:flex;align-items:center;justify-content:center;\">\n            <div style=\"width:68px;height:48px;background:rgba(255,0,0,0.85);border-radius:12px;\n              display:flex;align-items:center;justify-content:center;\">\n              <div style=\"width:0;height:0;border-left:18px solid #fff;\n                border-top:11px solid transparent;border-bottom:11px solid transparent;margin-left:4px;\">\n              </div>\n            </div>\n          </div>\n        </a>'''\n    body_content = re.sub(r'<iframe[^>]*youtube\\.com/embed[^>]*(?:/>|></iframe>)', replace_yt_iframe, body_content)\n    ```\n\n12. **Inject carousel fix** (from Step 3.5 findings):\n    - For stacked-grid carousels: hide all grid pages except the first via `:not(:first-child) { display: none !important; }`\n    - For infinite-scroll carousels: ensure animation keyframes are preserved and `overflow: hidden` is on the parent\n    - Never apply `transform: none !important` globally — it breaks carousel positioning\n\n12. **Inject canvas gradient replacement** (from Step 3.5 findings):\n    - For each detected canvas, identify the parent's background color\n    - Add a CSS `radial-gradient` on the parent to simulate the canvas glow effect\n    - Hide the empty canvas: `canvas.absolute { display: none !important; }`\n\n13. **Inject typewriter script** (from Step 3.6 captured prompts):\n\n```javascript\n// Typewriter animation\n(function() {\n  const prompts = [/* captured prompts from Step 3.6 */];\n  const span = document.querySelector('[class*=\"landing-input\"] span, [class*=\"input-box\"] span');\n  if (!span || !prompts.length) return;\n  let pi = 0, ci = 0, del = false;\n  function tick() {\n    const p = prompts[pi];\n    if (!del) { span.textContent = p.slice(0, ci); ci++; if (ci > p.length) { del = true; setTimeout(tick, 2000); return; } setTimeout(tick, 50); }\n    else { span.textContent = p.slice(0, ci); ci--; if (ci < 0) { del = false; ci = 0; pi = (pi + 1) % prompts.length; setTimeout(tick, 500); return; } setTimeout(tick, 30); }\n  }\n  setTimeout(tick, 1000);\n})();\n```\n\n14. **Add image fallback script**:\n\n```javascript\n// Fix scroll — use overflow:visible to avoid double scroll container\ndocument.body.style.setProperty('overflow', 'visible', 'important');\ndocument.documentElement.style.setProperty('overflow', 'visible', 'important');\ndocument.body.style.setProperty('height', 'auto', 'important');\ndocument.documentElement.style.setProperty('height', 'auto', 'important');\n\n// Fix h-screen wrapper (JS fallback for CSS specificity wars)\n(function() {\n  const wrapper = document.querySelector('[class*=\"h-screen\"][class*=\"w-screen\"]');\n  if (wrapper && wrapper.scrollHeight > wrapper.offsetHeight + 100) {\n    wrapper.style.setProperty('height', 'auto', 'important');\n    wrapper.style.setProperty('min-height', '100vh', 'important');\n    wrapper.style.setProperty('overflow', 'visible', 'important');\n  }\n})();\n\n// Fix lazy loaded images\ndocument.querySelectorAll('img[data-lazy-src],img[data-src]').forEach(img => {\n  const r = img.getAttribute('data-lazy-src') || img.getAttribute('data-src');\n  if (r && (!img.src || img.src.startsWith('data:'))) img.src = r;\n});\n\n// Fix broken srcset (relative URLs that weren't caught in assembly)\nconst DOMAIN = 'https://ORIGINAL_DOMAIN';  // replace during assembly\ndocument.querySelectorAll('img[srcset], source[srcset]').forEach(el => {\n  const srcset = el.getAttribute('srcset');\n  if (srcset && srcset.includes('/_next/')) {\n    el.setAttribute('srcset', srcset.replace(/(^|,\\s*)\\//g, '$1' + DOMAIN + '/'));\n  }\n});\n\n// Force visibility on animation-hidden elements\ndocument.querySelectorAll('[style*=\"opacity: 0\"]').forEach(el => {\n  // Don't un-hide elements that are intentionally hidden (opacity-0 class = SPA panels)\n  if (!(el.className || '').toString().includes('opacity-0')) {\n    el.style.opacity = '1';\n  }\n});\n\n// Fix cross-origin SVG image widths in marquee/logo strips\n// SVG images loaded from a different origin with CSS width:auto compute to 0px.\n// Calculate the correct width from HTML width/height attributes + CSS height.\ndocument.querySelectorAll('.animate-infinite-scroll img, [class*=\"animate-infinite\"] img').forEach(img => {\n  const attrW = parseInt(img.getAttribute('width'));\n  const attrH = parseInt(img.getAttribute('height'));\n  const cssH = parseFloat(getComputedStyle(img).height);\n  if (attrW && attrH && cssH && img.offsetWidth < 5) {\n    const w = Math.round((cssH / attrH) * attrW);\n    img.style.setProperty('width', w + 'px');\n    img.style.setProperty('min-width', w + 'px');\n    img.style.setProperty('flex-shrink', '0');\n  }\n});\n\n// Remove large SPA overlay panels that survived CSS extraction\ndocument.querySelectorAll('.opacity-0').forEach(el => {\n  if (el.offsetWidth > 400 && el.offsetHeight > 400) {\n    el.style.display = 'none';\n  }\n});\n```\n\n### Step 4.5 — Behavior Injection\n\nBased on recon findings (Step 1.5a, Step 3.4), inject lightweight JS scripts to bring\nthe static clone to life. These are zero-dependency, 10-30 line scripts injected before `</body>`.\n\n**Only inject scripts for behaviors actually detected.** Do not inject unused scripts.\n\n#### 4.5a — Scroll Reveal Animations\n\nIf `scroll_behaviors.scroll_reveals` is non-empty, inject an IntersectionObserver that\nreplays fade-in/slide-up animations as the user scrolls:\n\n```javascript\n// Scroll reveal — triggers when element enters viewport\n(function() {\n  const style = document.createElement('style');\n  style.textContent = `\n    .clone-reveal { opacity: 0; transform: translateY(20px); transition: opacity 0.6s ease, transform 0.6s ease; }\n    .clone-reveal.visible { opacity: 1; transform: none; }\n  `;\n  document.head.appendChild(style);\n\n  // Target elements that had scroll-reveal animations (detected in Step 1.5a)\n  const selectors = '[class*=\"animate-\"], [data-aos], [data-scroll], [data-sal], [class*=\"reveal\"], [class*=\"fade-in\"]';\n  document.querySelectorAll(selectors).forEach(el => {\n    el.classList.add('clone-reveal');\n    // Remove the force-visible overrides so animation can play\n    el.style.removeProperty('opacity');\n    el.style.removeProperty('transform');\n  });\n\n  const observer = new IntersectionObserver((entries) => {\n    entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); } });\n  }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });\n\n  document.querySelectorAll('.clone-reveal').forEach(el => observer.observe(el));\n})();\n```\n\n**Also update the CSS fix overrides:** remove the blanket `[class*=\"animate-\"] { animation: none; opacity: 1; transform: none; }` and replace with:\n\n```css\n/* Let scroll-reveal animations be driven by JS IntersectionObserver instead */\n[class*=\"animate-\"] {\n  animation: none !important;\n  /* opacity and transform are NOW controlled by .clone-reveal JS, not forced to 1/none */\n}\n```\n\n#### 4.5b — Tab/Accordion Click Toggle\n\nIf `click_behaviors` detected tabs or accordions, inject a click handler:\n\n```javascript\n// Tab switching\n(function() {\n  document.querySelectorAll('[role=\"tablist\"]').forEach(tablist => {\n    const tabs = tablist.querySelectorAll('[role=\"tab\"]');\n    const panels = document.querySelectorAll('[role=\"tabpanel\"]');\n    tabs.forEach((tab, i) => {\n      tab.addEventListener('click', () => {\n        tabs.forEach(t => t.setAttribute('data-state', 'inactive'));\n        panels.forEach(p => { p.hidden = true; p.setAttribute('data-state', 'inactive'); });\n        tab.setAttribute('data-state', 'active');\n        if (panels[i]) { panels[i].hidden = false; panels[i].setAttribute('data-state', 'active'); }\n      });\n    });\n  });\n\n  // Accordion toggle (details/summary already work natively, this handles data-state pattern)\n  document.querySelectorAll('[data-state=\"closed\"], [data-state=\"open\"]').forEach(el => {\n    const trigger = el.querySelector('button, [role=\"button\"]') || el;\n    trigger.addEventListener('click', () => {\n      const current = el.getAttribute('data-state');\n      el.setAttribute('data-state', current === 'open' ? 'closed' : 'open');\n      const content = el.querySelector('[data-state]') ||\n                      el.nextElementSibling;\n      if (content) {\n        content.style.display = current === 'open' ? 'none' : 'block';\n        content.style.height = current === 'open' ? '0' : 'auto';\n      }\n    });\n  });\n})();\n```\n\n#### 4.5c — Sticky Header Scroll Effect\n\nIf `scroll_behaviors.sticky` detected a sticky/fixed header, inject a scroll listener\nthat adds/removes a class for the scrolled state:\n\n```javascript\n// Sticky header — add shadow/bg change on scroll\n(function() {\n  const header = document.querySelector('header, nav');\n  if (!header) return;\n  const cls = 'clone-header-scrolled';\n  const style = document.createElement('style');\n  style.textContent = `.${cls} { backdrop-filter: blur(12px) !important; box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important; }`;\n  document.head.appendChild(style);\n\n  window.addEventListener('scroll', () => {\n    if (window.scrollY > 50) header.classList.add(cls);\n    else header.classList.remove(cls);\n  }, { passive: true });\n})();\n```\n\n#### 4.5d — Smooth Scroll\n\nIf `media_precheck.scroll_libs` detected a smooth scroll library, or as a general\nenhancement, add CSS smooth scrolling:\n\n```css\n/* Smooth scroll behavior */\nhtml { scroll-behavior: smooth; }\n```\n\nIf Lenis was detected, optionally inject a mini smooth-scroll script:\n\n```javascript\n// Lightweight smooth scroll (replaces Lenis for basic feel)\n// Only inject if Lenis was detected in Step 1.5d\n(function() {\n  document.querySelectorAll('a[href^=\"#\"]').forEach(a => {\n    a.addEventListener('click', e => {\n      const target = document.querySelector(a.getAttribute('href'));\n      if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); }\n    });\n  });\n})();\n```\n\n#### 4.5e — Parallax Effect\n\nIf `scroll_behaviors` detected parallax layers (elements with different scroll rates),\ninject a lightweight parallax handler:\n\n```javascript\n// Simple parallax on background elements\n(function() {\n  const parallaxEls = document.querySelectorAll('[class*=\"parallax\"], [data-parallax], [class*=\"bg-fixed\"]');\n  if (!parallaxEls.length) return;\n  window.addEventListener('scroll', () => {\n    const scrollY = window.scrollY;\n    parallaxEls.forEach(el => {\n      const rate = parseFloat(el.dataset.parallaxRate || '0.3');\n      el.style.transform = 'translateY(' + (scrollY * rate) + 'px)';\n    });\n  }, { passive: true });\n})();\n```\n\n#### 4.5f — Stacked-Grid Paged Carousel (Dot Navigation)\n\nIf Step 3.5 detected `stacked-grid` carousels (multiple same-class grid containers stacked\non top of each other with absolute positioning), inject a paged carousel with dot indicators\nand auto-rotation. Common pattern: example/showcase cards below hero with 3 cards per page.\n\n```javascript\n// Stacked-grid paged carousel with dot navigation + auto-rotate\n(function() {\n  // Find grids: multiple .grid containers inside the same parent, stacked via absolute position\n  // Selector should be adapted based on Step 3.5 detection (section class, grid class)\n  const section = document.querySelector('DETECTED_SECTION_SELECTOR');\n  if (!section) return;\n\n  const grids = Array.from(section.querySelectorAll('.grid.grid-cols-1'));\n  if (grids.length < 2) return;\n\n  // Find dot indicators (small round buttons)\n  const dots = Array.from(section.querySelectorAll('button')).filter(b =>\n    b.offsetWidth <= 12 && (b.className||'').includes('rounded-full')\n  );\n\n  let current = 0;\n  const total = grids.length;\n\n  function show(idx) {\n    grids.forEach((g, i) => {\n      if (i === idx) {\n        g.style.opacity = '1';\n        g.style.pointerEvents = 'auto';\n        g.style.position = 'relative';\n        g.style.zIndex = '1';\n      } else {\n        g.style.opacity = '0';\n        g.style.pointerEvents = 'none';\n        g.style.position = 'absolute';\n        g.style.zIndex = '0';\n      }\n    });\n    dots.forEach((d, i) => {\n      d.style.backgroundColor = i === idx ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.15)';\n    });\n    current = idx;\n  }\n\n  // CSS transition for smooth fade\n  const style = document.createElement('style');\n  style.textContent = 'SECTION_SELECTOR .grid { transition: opacity 0.5s ease !important; top:0; left:0; right:0; }';\n  document.head.appendChild(style);\n\n  show(0);\n\n  // Dot click handlers\n  dots.forEach((d, i) => {\n    d.addEventListener('click', () => {\n      show(i % total);\n      clearInterval(autoTimer);\n      autoTimer = setInterval(next, 4000);\n    });\n    d.style.cursor = 'pointer';\n  });\n\n  // Auto-rotate every 4 seconds\n  function next() { show((current + 1) % total); }\n  let autoTimer = setInterval(next, 4000);\n})();\n```\n\n**Detection criteria:** Step 3.5 `stacked-grid` type with `pages > 1`, AND dot buttons found\nin the same section (small round buttons with `rounded-full` class, width ≤ 12px).\n\n#### Injection Decision Table\n\n| Detected in Recon | Script to Inject | Size |\n|-------------------|-----------------|------|\n| `scroll_reveals.length > 0` | 4.5a Scroll Reveal | ~25 lines |\n| `click_behaviors` has tabs/accordion | 4.5b Tab/Accordion Toggle | ~25 lines |\n| `sticky.length > 0` | 4.5c Sticky Header | ~12 lines |\n| `scroll_libs.length > 0` OR always | 4.5d Smooth Scroll | ~8 lines |\n| parallax detected | 4.5e Parallax | ~10 lines |\n| stacked-grid + dots detected | 4.5f Paged Carousel | ~40 lines |\n| typewriters detected | Step 13 Typewriter (existing) | ~15 lines |\n| carousels detected | Step 12 Carousel fix (existing) | existing |\n\n**Only inject what was detected.** A site with no scroll animations gets no scroll-reveal script.\n\n**Write the assembled HTML to the output path.**\n\n---\n\n### Level 1a — Hybrid: Original CSS + Targeted Overrides (~90% fidelity)\n\n**Preferred fix when Level 1 produces a blank/broken page** due to Tailwind `important: true`\nor large invisible overlay panels covering content. Preserves responsive layout.\n\n**Root cause:** Tailwind `important: true` makes ALL utility classes use `!important`, so\n`.opacity-0 { opacity: 0 !important }` overrides even inline `style=\"opacity:1 !important\"`.\nAdditionally, SPA sites often have large editor/chat panels (opacity: 0, full-viewport-sized)\nthat sit on top of the landing page content in the DOM.\n\n**How it works:** Same as Level 1 (keep original CSS), but:\n1. Remove large invisible overlay panels from the DOM before extraction\n2. Add targeted CSS `!important` overrides for common Tailwind conflicts\n3. Add a cleanup script that hides remaining opacity-0 panels at runtime\n\n**Step 1 — Remove overlays (already done in Step 2 Force Visibility)**\n\nThe overlay removal in Step 2 handles this. Verify after extraction that\n`document.elementFromPoint(720, 400)` returns actual content, not an overlay div.\n\n**Step 2 — Add targeted CSS overrides**\n\nIn the CSS fix overrides section (Step 4), add:\n\n```css\n/* === TAILWIND IMPORTANT:TRUE FIXES === */\n/* Force hero text elements visible despite Tailwind !important cascade */\nsection h2, section h1, section p, section span, section a {\n  opacity: 1 !important;\n  visibility: visible !important;\n}\n\n/* Hide large SPA overlay panels that have opacity-0 class */\n.opacity-0 { display: none !important; }\n\n/* Contain hero background gradient (don't let it overflow) */\nsection:first-of-type { overflow: hidden !important; }\n```\n\n**Step 3 — Add runtime cleanup script**\n\n```javascript\n// Remove large editor overlays that survived CSS extraction\ndocument.querySelectorAll('.opacity-0').forEach(el => {\n  if (el.offsetWidth > 400 && el.offsetHeight > 400) {\n    el.style.display = 'none';\n  }\n});\n```\n\n**Tradeoffs vs Level 1:**\n- (+) Preserves responsive layout and CSS breakpoints\n- (+) Small file size (~800 KB, same as Level 1)\n- (+) Editable via CSS classes\n- (-) May not fix ALL Tailwind conflicts (some elements may still be invisible)\n- (-) If hero is still blank after 1a, escalate to Level 1b\n\n**When to escalate to 1b:** If after applying 1a overrides, screenshotting the clone still\nshows a blank hero (no text visible at y=300-500), escalate to Level 1b.\n\n---\n\n### Level 1b — Playwright Targeted Style Bake (~80% fidelity)\n\n**Last resort for Playwright-available sites.** Use when Level 1a still produces a blank page.\nBakes computed styles into inline attributes, but uses a curated property list (not all properties)\nto avoid CSS custom property pollution and keep file size reasonable.\n\n**How it works:** Instead of iterating ALL computed properties (which includes Tailwind v4 custom\nproperties like `--color-purple-200` that pollute inline styles), use a curated list of ~60\nessential visual properties.\n\n```python\n    ESSENTIAL_PROPS = [\n        'display', 'position', 'top', 'right', 'bottom', 'left',\n        'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',\n        'margin-top', 'margin-right', 'margin-bottom', 'margin-left',\n        'padding-top', 'padding-right', 'padding-bottom', 'padding-left',\n        'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',\n        'justify-content', 'align-items', 'align-self', 'align-content',\n        'gap', 'row-gap', 'column-gap',\n        'grid-template-columns', 'grid-template-rows', 'grid-column', 'grid-row',\n        'font-family', 'font-size', 'font-weight', 'font-style', 'line-height',\n        'letter-spacing', 'text-align', 'text-decoration', 'text-transform',\n        'text-overflow', 'white-space', 'word-break',\n        'color', 'background-color', 'background-image', 'background-size',\n        'background-position', 'background-repeat',\n        'background-clip', '-webkit-background-clip', '-webkit-text-fill-color',\n        'border-top-left-radius', 'border-top-right-radius',\n        'border-bottom-left-radius', 'border-bottom-right-radius',\n        'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',\n        'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',\n        'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',\n        'box-shadow', 'text-shadow',\n        'opacity', 'visibility', 'z-index', 'overflow', 'overflow-x', 'overflow-y',\n        'transform', 'transform-origin',\n        'object-fit', 'object-position',\n        'aspect-ratio', 'box-sizing', 'isolation', 'vertical-align',\n        'backdrop-filter', '-webkit-backdrop-filter',\n        'mix-blend-mode', 'filter',\n        '-webkit-line-clamp', 'will-change', 'cursor',\n        'transition', 'transition-property', 'transition-duration',\n    ]\n\n    # After page is fully loaded and scrolled...\n    page.evaluate(f\"\"\"() => {{\n        const props = {json.dumps(ESSENTIAL_PROPS)};\n        const skip = new Set(['SCRIPT','STYLE','LINK','META','HEAD','TITLE','NOSCRIPT','BR','HR']);\n\n        document.querySelectorAll('*').forEach(el => {{\n            if (skip.has(el.tagName)) return;\n            const cs = getComputedStyle(el);\n            const styles = [];\n\n            for (const prop of props) {{\n                const val = cs.getPropertyValue(prop);\n                if (!val || val === '') continue;\n                // Skip default/uninteresting values to reduce size\n                if (val === 'none' && !['display','text-decoration','transform','box-shadow'].includes(prop)) continue;\n                if (val === 'auto' && ['top','right','bottom','left','width','height','z-index'].includes(prop)) continue;\n                if (val === 'static' && prop === 'position') continue;\n                if (val === 'visible' && ['overflow','overflow-x','overflow-y','visibility'].includes(prop)) continue;\n                if (val === '1' && prop === 'opacity') continue;\n                if (val === 'rgba(0, 0, 0, 0)' && prop === 'background-color') continue;\n                if (val === '0px' && (prop.startsWith('margin') || prop.endsWith('-radius') || prop.endsWith('-width'))) continue;\n                styles.push(prop + ':' + val);\n            }}\n\n            // Handle gradient text (background-clip: text + transparent fill)\n            const bgClip = cs.getPropertyValue('-webkit-background-clip');\n            const textFill = cs.getPropertyValue('-webkit-text-fill-color');\n            if (bgClip === 'text' && textFill === 'transparent') {{\n                const bgImg = cs.getPropertyValue('background-image');\n                if (bgImg && bgImg !== 'none') {{\n                    styles.push('background-image:' + bgImg);\n                    styles.push('-webkit-background-clip:text');\n                    styles.push('background-clip:text');\n                    styles.push('-webkit-text-fill-color:transparent');\n                }}\n            }}\n\n            el.setAttribute('style', styles.join(';'));\n        }});\n    }}\"\"\")\n```\n\n**Assembly:** Same as Level 1 Step 4, but:\n- **Do NOT include any `<style>` blocks or CSS `<link>` tags** (all styles are inline)\n- **Only keep `@font-face` rules** and `@keyframes` rules for custom fonts and animations\n- Strip `<script>`, fix relative URLs, fix lazy images as usual\n- **Post-process:** Replace baked viewport-width values (e.g., `width:1440px`) with `width:100%`\n\n**Tradeoffs vs Level 1/1a:**\n- (+) Works when CSS cascade is completely broken\n- (+) No CSS custom property pollution (curated property list)\n- (-) File size ~1-2 MB (much smaller than naive full bake which produces 20MB+)\n- (-) Responsive breakpoints are lost (styles are baked at extraction viewport size)\n- (-) Editing requires changing inline styles directly (no class-based editing)\n\n**Filename:** `clone-[domain].html` (same as Level 1)\n\n---\n\n## Phase 3 — Visual QA (3-Round Protocol)\n\nStandardized 3-round verification. Do NOT skip rounds or declare the clone complete\nuntil all 3 rounds pass.\n\n**Round 1 — Desktop section-by-section** (1440px): screenshot each section at its\ntopology boundary, compare original vs clone side-by-side. Fix discrepancies.\n\n**Round 2 — Mobile full-page** (390px): screenshot original and clone at mobile width,\ncompare full-page. Fix responsive breakage (columns not stacking, overflow, font scaling).\n\n**Round 3 — Interaction verification**: verify carousel animations, typewriter cycling,\nvideo elements, and SVG visibility in the clone. Fix broken interactions.\n\n### Round 1 — Desktop Section-by-Section (1440px)\n\n```python\nwith sync_playwright() as p:\n    browser = p.chromium.launch(headless=True)\n\n    # Screenshot clone (use \"load\" — file:// URLs have no network activity for networkidle)\n    page_c = browser.new_page(viewport={\"width\": 1440, \"height\": 900})\n    page_c.goto(f\"file://{output_path}\", wait_until=\"load\")\n    page_c.wait_for_timeout(3000)\n    page_c.screenshot(path=\"/tmp/clone-viewport.png\")\n    page_c.screenshot(path=\"/tmp/clone-full.png\", full_page=True)\n\n    clone_height = page_c.evaluate(\"() => document.body.scrollHeight\")\n    clone_sections = page_c.evaluate(\"\"\"() =>\n        Array.from(document.querySelectorAll('section')).map(s => ({\n            cls: s.className.slice(0,50), h: s.offsetHeight\n        }))\n    \"\"\")\n\n    browser.close()\n```\n\n### Round 2 — Mobile Full-Page (390px)\n\nIf responsive breakpoints were detected in Step 1.5a, also compare at mobile width.\n\n```python\n    if recon.get('responsive_info', {}).get('count', 0) > 0:\n        with sync_playwright() as p:\n            browser = p.chromium.launch(headless=True)\n\n            # Mobile screenshot of original\n            page_m = browser.new_page(viewport={\"width\": 390, \"height\": 844})\n            try:\n                page_m.goto(TARGET_URL, wait_until=\"networkidle\", timeout=30000)\n            except:\n                page_m.goto(TARGET_URL, wait_until=\"domcontentloaded\", timeout=60000)\n            page_m.wait_for_timeout(5000)\n            page_m.screenshot(path=\"/tmp/clone-original-mobile.png\", full_page=True)\n            page_m.close()\n\n            # Mobile screenshot of clone\n            page_mc = browser.new_page(viewport={\"width\": 390, \"height\": 844})\n            page_mc.goto(f\"file://{output_path}\", wait_until=\"load\")\n            page_mc.wait_for_timeout(3000)\n            page_mc.screenshot(path=\"/tmp/clone-mobile.png\", full_page=True)\n            page_mc.close()\n\n            browser.close()\n```\n\nCompare desktop AND mobile screenshots. If mobile layout is broken (columns not stacking,\nhamburger menu not collapsing), add targeted `@media (max-width: 768px)` fixes.\n\n### Round 1 & 2 — Diagnose Issues\n\nCheck the following common problems:\n\n| Symptom | Root Cause | Fix |\n|---------|-----------|-----|\n| Sections have 0px height | `content-visibility: auto` | Add `content-visibility: visible !important` |\n| Body not scrollable | `overflow: hidden` on body/html | Override to `overflow: visible !important` (NOT `auto` — see below) |\n| Body not scrollable (still) | Double scroll container: both html and body have `overflow-y: auto` | Use `overflow: visible !important` on both — `auto` on both creates two nested scroll contexts where `window.scrollTo` only affects html |\n| Page stuck at viewport height | `div.h-screen.w-screen` wrapper (Next.js/SPA) | Strip `h-screen`/`w-screen` classes in HTML assembly + CSS override + JS fallback (see Step 4 item 2) |\n| Page stuck at viewport (still) | Tailwind `important:true` → `.ck-style .h-screen { height: 100vh !important }` wins over simple selectors | Use `[class*=\"ck-style\"] .h-screen` selector in CSS + JS `setProperty('height','auto','important')` fallback |\n| Logo marquee broken layout | `[class*=\"animate-\"] { display: revert }` converts flex→block | Remove `display: revert` from animate- override |\n| Logo marquee not sliding | `[class*=\"animate-\"] { animation: none }` kills infinite-scroll | Exclude with `:not([class*=\"animate-infinite\"]):not([class*=\"animate-marquee\"])` |\n| Logo marquee images invisible | Cross-origin SVG `width:auto` computes to 0px | JS fix: calculate width from HTML `width`/`height` attrs + CSS height (see Step 14) |\n| Images blank | Lazy loading with `data-lazy-src` | Replace `src` with `data-lazy-src` value |\n| Content invisible | `opacity: 0` from animation init state | Force `opacity: 1 !important` |\n| Elements pushed off-screen | `transform: translateY(100%)` | Force `transform: none !important` |\n| Page very short | Most content in `display:none` | Check if animation JS needed to reveal |\n\n### Round 1 & 2 — Section-Based Comparison\n\nRead both screenshots and compare side-by-side:\n- **Viewport screenshot** — first-screen impression (navbar, hero, above-fold)\n- **Full-page screenshot** — all sections, overall structure\n\nFor detailed comparison, use the topology map from Step 1.5b for section-based comparison:\n\n```python\n# Use topology-driven section boundaries instead of fixed intervals\nscroll_points = [s['y'] for s in recon.get('topology', []) if s['h'] > 100]\nif not scroll_points:\n    scroll_points = [0, 1200, 3000, 5000, 8000, 11000, 14000]  # fallback\n\nfor y in scroll_points:\n    # Screenshot clone at y\n    # Screenshot original at y\n    # Compare pair — each comparison targets a specific named section\n```\n\nThis ensures every section gets compared regardless of page length, and failures\nare reported with section names (from topology) instead of anonymous scroll offsets.\n\n### Round 1 & 2 — Fix & Re-verify\n\nFor each visual difference found:\n1. Identify the CSS rule or DOM element causing the difference\n2. Add a targeted CSS override or DOM fix\n3. Re-screenshot to verify the fix\n4. Repeat until the pair matches\n\n**Common fixes:**\n- Wrong background color → check body/section inline styles\n- Missing rounded corners → sections need `border-radius` from original CSS\n- Wrong brand color → check button/CTA actual computed color\n- Content misaligned → check container max-width and padding values\n- Fonts wrong → verify Google Fonts link is preserved\n\n### Round 3 — Interaction Behavior Verification\n\nIf behavior sweep (Step 1.5a) or interactive detection (Step 3.5) found interactive elements,\nverify they work correctly in the clone. Use `getBoundingClientRect()` (not `offsetWidth`)\nfor SVG elements:\n\n```python\n    with sync_playwright() as p:\n        browser = p.chromium.launch(headless=True)\n        page_v = browser.new_page(viewport={\"width\": 1440, \"height\": 900})\n        page_v.goto(f\"file://{output_path}\", wait_until=\"load\")\n        page_v.wait_for_timeout(3000)\n\n        verification = {}\n\n        # Verify carousel/marquee animation is running\n        if interactive.get('carousels'):\n            verification['carousel'] = page_v.evaluate(\"\"\"async () => {\n                const el = document.querySelector('[class*=\"carousel\"] [style*=\"transform\"], [class*=\"marquee\"], [class*=\"ticker\"]');\n                if (!el) return 'no-element';\n                const t1 = getComputedStyle(el).transform;\n                await new Promise(r => setTimeout(r, 2000));\n                return getComputedStyle(el).transform !== t1 ? 'animating' : 'static';\n            }\"\"\")\n\n        # Verify typewriter text changes\n        if interactive.get('typewriters'):\n            verification['typewriter'] = page_v.evaluate(\"\"\"async () => {\n                const span = document.querySelector('[class*=\"landing-input\"] span, [class*=\"input-box\"] span');\n                if (!span) return 'no-element';\n                const t1 = span.textContent;\n                await new Promise(r => setTimeout(r, 3000));\n                return span.textContent !== t1 ? 'typing' : 'static';\n            }\"\"\")\n\n        # Verify video elements have valid src\n        if recon.get('media_precheck', {}).get('videos'):\n            verification['videos'] = page_v.evaluate(\"\"\"() =>\n                [...document.querySelectorAll('video')].map(v => ({\n                    src: v.src || v.querySelector('source')?.src || 'missing',\n                    w: v.offsetWidth\n                }))\n            \"\"\")\n\n        page_v.close()\n        browser.close()\n```\n\nInclude `verification` results in Phase 4 clone report under a **BEHAVIOR CHECK** section.\n\n---\n\n## Phase 2 (continued) — Fallback Levels\n\nIf Level 1 (Playwright) is unavailable, use the highest available level below.\nAfter extraction by any level, proceed to Phase 3 for screenshot comparison (if Playwright\nis available for verification even though it failed for extraction, or skip Phase 3 if not).\n\n### Level 2 — Firecrawl + CSS Download (~90% fidelity)\n\n**When:** Playwright unavailable, Firecrawl available.\n\nFirecrawl uses a headless browser internally, so it renders JS. But it returns HTML as a string\nrather than giving access to computed styles. We compensate by downloading external CSS files.\n\n**Step 1 — Multi-format scrape**\n\n```bash\n# Get rendered HTML (JS-executed DOM) — preserves DOM structure\nfirecrawl scrape \"TARGET_URL\" -f html --wait-for 5000 -o /tmp/site-html.html\n\n# Get raw HTML with all styles — includes <style> blocks and <link> tags\nfirecrawl scrape \"TARGET_URL\" -f rawHtml --wait-for 5000 -o /tmp/site-raw.html\n\n# Get markdown — clean text content for copy verification\nfirecrawl scrape \"TARGET_URL\" -f markdown --wait-for 5000 -o /tmp/site-md.md\n```\n\n**Step 2 — Download external CSS files**\n\nExtract all `<link rel=\"stylesheet\">` URLs from rawHtml, download each with `curl`:\n\n```bash\ncurl -sL \"[css-url]\" -o /tmp/site-main.css\n```\n\nThe main framework CSS (usually 100KB+) contains layout rules, responsive breakpoints, and component styles.\nThis is where critical values like `border-radius`, `margin`, `background-color`, `font-family` live.\n\n**Step 3 — Extract design tokens from CSS**\n\nParse downloaded CSS to find rules for key selectors:\n- Section-level: `section_*`, `nav_*`, `hero*`, `footer*`, `cta*`\n- Spacing: `container*`, `padding-global`, `padding-section*`\n- Typography: `heading-style*`, `text-size*`, `playfair*`, `font-*`\n- Buttons: `btn*`, `button*`, `nav-btn` — **especially brand colors!**\n- Components: `accordion*`, `pricing*`, `comparison*`, `faq*`, `gallery*`\n\n**Step 4 — Assemble self-contained HTML**\n\nSame assembly process as Level 1 Step 4: combine Firecrawl's HTML body + downloaded CSS into a single file.\nAdd the same `content-visibility`, `overflow`, and lazy-image fix overrides.\n\n**Step 5 — Screenshot comparison (if Playwright available for verification)**\n\nIf Playwright is available for screenshots but failed for the initial extraction (e.g., site blocked\nheadless Chrome but Firecrawl's browser succeeded), use Playwright to screenshot the clone and compare.\n\n---\n\n### Level 3 — gstack/browse Screenshot + DOM (~85% fidelity)\n\n**When:** Playwright and Firecrawl unavailable, gstack or browse daemon available.\n\n**Step 1 — Screenshot the original**\n\n```bash\ngstack screenshot \"TARGET_URL\" --full-page --output /tmp/original.png 2>/dev/null \\\n  || browse screenshot \"TARGET_URL\" --output /tmp/original.png 2>/dev/null\n```\n\n**Step 2 — Extract rendered DOM**\n\n```bash\ngstack eval \"TARGET_URL\" \"document.documentElement.outerHTML\" > /tmp/site-dom.html 2>/dev/null \\\n  || browse eval \"TARGET_URL\" \"document.documentElement.outerHTML\" > /tmp/site-dom.html 2>/dev/null\n```\n\n**Step 3 — Download external CSS**\n\nSame as Level 2 Step 2 — extract stylesheet URLs from DOM, download with curl.\n\n**Step 4 — Assemble HTML**\n\nSame as Level 1 Step 4 assembly, using the extracted DOM + downloaded CSS.\n\n**Step 5 — Visual comparison using screenshot**\n\nRead the original screenshot visually (via the Read tool) and compare with the clone.\nUse gstack/browse to screenshot the clone for side-by-side comparison.\n\n**Fidelity gap vs Level 1-2:** gstack/browse may not wait for all JS to finish, so some\ndynamic content might be missing. CSS extraction is the same quality.\n\n---\n\n### Level 4 — WebFetch + Manual Reconstruction (~75% fidelity)\n\n**When:** No headless browser tools available. Only WebFetch (HTTP fetch).\n\nThis level **cannot render JS**. It works for static HTML sites but produces degraded results for SPAs.\n\n**Step 1 — Fetch the page**\n\n```\nWebFetch(TARGET_URL, \"Return the complete HTML structure of this page.\nList every section, every CSS class, every inline style, every color value,\nevery font reference, every image URL. Be exhaustive.\")\n```\n\n**Step 2 — Detect SPA**\n\nCheck the WebFetch result for SPA signals:\n- `<div id=\"root\">` or `<div id=\"app\">` with empty/minimal content\n- `<noscript>` with substantial fallback\n- Fewer than 200 words of visible text\n- Only script bundles as `<script src>`\n\nIf SPA detected → **escalate to Level 5** (WebFetch cannot render JS).\n\n**Step 3 — Extract design details via WebFetch**\n\nMake a second WebFetch call focused on design extraction:\n\n```\nWebFetch(TARGET_URL, \"Extract all design details:\n1. Every hex/rgb color value  2. Font families and sizes\n3. Spacing values  4. Border-radius values  5. Box-shadow values\n6. Exact text content of every heading, paragraph, button\n7. Layout structure (grid columns, flex directions)\n8. Background gradients  9. Image URLs\")\n```\n\n**Step 4 — Download external CSS**\n\nAttempt to fetch CSS files directly:\n\n```\nWebFetch(\"[css-url]\", \"Return the raw CSS content of this stylesheet\")\n```\n\n**Step 5 — Manual code generation**\n\nUsing the extracted information, manually write the HTML/CSS clone:\n1. Build semantic HTML structure matching the detected sections\n2. Apply extracted CSS values (colors, fonts, spacing, etc.)\n3. Use real image URLs where available\n4. Mark estimated values with `/* estimated */` comments\n\n**Fidelity gap:** Layout proportions, exact spacing, responsive breakpoints, and interactive\ncomponents will be approximated. Typography may be wrong if fonts are loaded via JS.\n\n---\n\n### Level 5 — WebSearch Research Only (~50% fidelity)\n\n**When:** WebFetch also fails (site blocks scraping, is behind auth, or returns empty content).\n\n**Step 1 — Search for visual references**\n\nRun in parallel:\n```\nWebSearch(\"[domain] website screenshot\")\nWebSearch(\"[domain] design system\")\nWebSearch(\"[domain] color palette hex\")\nWebSearch(\"[domain] typography font\")\nWebSearch(\"site:figma.com [domain]\")\nWebSearch(\"site:dribbble.com [domain]\")\n```\n\n**Step 2 — Gather design intelligence**\n\nFrom search results, collect:\n- Screenshots or preview images (e.g., from ProductHunt, Awwwards, Dribbble)\n- Brand guidelines or design system documentation\n- Color palette (from brand resources or CSS analysis tools)\n- Typography choices\n\n**Step 3 — Manual reconstruction from research**\n\nBuild the clone based on collected intelligence. **Mark ALL values as estimated:**\n\n```css\n:root {\n  --color-bg: #ffffff; /* estimated */\n  --color-accent: #4f46e5; /* estimated from screenshots */\n}\n```\n\n**Fidelity gap:** Everything is approximated. Layout, spacing, interactions, and responsive behavior\nare best-effort guesses. This is a last resort.\n\n**Always warn the user:**\n```\n⚠️ Level 5 fallback — clone is based on web research, not direct extraction.\nAll values are estimated. Fidelity will be significantly lower (~50%).\nFor better results, ensure Playwright or Firecrawl is installed.\n```\n\n---\n\n## Phase 4 — Output & Report\n\n### Write the file\n\nWrite to `$(pwd)/test_outputs/clone-[domain].html`.\n\n### Clone Report\n\nSave this report as a markdown file alongside the HTML output, using the same filename with `.md` extension\n(e.g., `clone-obsidian.html` → `clone-obsidian.md`). Then output the report to the user:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nCLONE REPORT\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nSource              [TARGET_URL]\nMethod              [Playwright DOM extraction / Firecrawl + manual]\nSections cloned     [N/N]\nPage height         [Xpx] (original: [Ypx])\nOutput file         [full path]\nOutput size         [X KB]\n\nSECTION VERIFICATION\n  [section_name]: [height]px ✓\n  [section_name]: [height]px ✓\n  ...\n\nRECON SUMMARY\n  Sections: [N] | Scroll animations: [N] | Hover elements: [N]\n  Tabs/Accordions: [N] | Responsive breakpoints: [list]\n  Videos: [N] | Lottie: [N] | Scroll lib: [name/none]\n  Layered stacks: [N] | SVGs: [N] total ([N] icons)\n\nBEHAVIOR CHECK\n  Carousel: [animating/static/no-element]\n  Typewriter: [typing/static/no-element]\n  Videos: [N valid / N total]\n\nKNOWN DEVIATIONS\n  - [each deviation and why]\n  - [e.g. \"iframe content blank — cross-origin, works when deployed to web server\"]\n  - [e.g. \"FAQ accordion fully expanded — original uses JS to toggle\"]\n\nTO REFINE\n  Describe what doesn't match and I'll fix that specific section.\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n```\n\n### Cleanup Intermediate Files\n\nAfter the report, remove all intermediate screenshots generated during QA rounds.\nOnly the final `.html` clone file and its `.md` report should remain in `test_outputs/`.\n\n```bash\n# Remove all intermediate PNG screenshots from test_outputs\nrm -f \"$(pwd)/test_outputs/\"*.png\n```\n\nThis prevents accumulation of debug artifacts (viewport screenshots, section comparisons,\noriginal vs clone diffs) that are only useful during the QA process.\n\n---\n\n## Optional — Asset Download (user-requested)\n\n**Trigger:** User says \"download assets\", \"save locally\", \"offline\", or \"self-contained with images\".\n\nDownload all referenced assets to a local directory and rewrite URLs in the clone.\n\n```python\nimport os, re, urllib.parse\n\nasset_dir = os.path.join(os.path.dirname(output_path), 'assets-' + domain)\nos.makedirs(asset_dir, exist_ok=True)\n\n# Extract all asset URLs from clone HTML\nurls = set()\nfor match in re.findall(r'(?:src|href|poster|url\\()[\\s=\"\\']*([^\"\\')\\s>]+)', clone_html):\n    if match.startswith('http') and any(ext in match.lower() for ext in\n        ['.png','.jpg','.jpeg','.webp','.svg','.gif','.mp4','.webm','.woff','.woff2','.ttf','.otf','.ico']):\n        urls.add(match)\n```\n\n```bash\n# Download each asset (parallel with xargs for speed)\necho \"$URLS\" | xargs -P 4 -I {} sh -c 'curl -sL \"{}\" -o \"ASSET_DIR/$(basename \"{}\" | cut -d\"?\" -f1)\"'\n\n# Also grab favicons\ncurl -sL \"https://DOMAIN/favicon.ico\" -o \"ASSET_DIR/favicon.ico\" 2>/dev/null\ncurl -sL \"https://DOMAIN/apple-touch-icon.png\" -o \"ASSET_DIR/apple-touch-icon.png\" 2>/dev/null\n```\n\nAfter download, rewrite URLs in the clone HTML to relative paths: `./assets-[domain]/[filename]`.\n\n---\n\n## Optional — Design Token Export (user-requested)\n\n**Trigger:** User says \"design tokens\", \"extract tokens\", or \"design system\".\n\nStructure the `:root` custom properties (from Step 3c) into a categorized JSON file.\n\n```python\nimport json\n\ntokens = {\"colors\": {}, \"spacing\": {}, \"typography\": {}, \"radii\": {}, \"shadows\": {}, \"other\": {}}\nfor prop, val in root_vars.items():\n    if any(k in prop for k in ['color', 'bg', 'foreground', 'background', 'border-color', 'accent', 'primary', 'secondary', 'destructive', 'muted', 'chart']):\n        tokens['colors'][prop] = val\n    elif any(k in prop for k in ['radius', 'rounded']):\n        tokens['radii'][prop] = val\n    elif any(k in prop for k in ['shadow']):\n        tokens['shadows'][prop] = val\n    elif any(k in prop for k in ['font', 'text', 'line-height', 'letter-spacing']):\n        tokens['typography'][prop] = val\n    elif any(k in prop for k in ['spacing', 'gap', 'padding', 'margin']):\n        tokens['spacing'][prop] = val\n    else:\n        tokens['other'][prop] = val\n\ntoken_path = output_path.replace('.html', '-tokens.json')\n# Write to test_outputs/design-tokens-[domain].json\n```\n\n---\n\n## Refinement Flow\n\nIf the user says a section doesn't match, or wants adjustments:\n\n1. Re-screenshot the specific section from both original and clone\n2. Compare side-by-side to identify the exact difference\n3. Fix ONLY the identified issue — do not touch other sections\n4. Re-screenshot to verify the fix\n5. Report what changed\n\n**Never \"improve\" during refinement.** The user is asking for closer fidelity, not redesign.\n\n---\n\n## Phase 5 — Next.js Project Output (Optional)\n\n**Trigger:** User explicitly requests \"Next.js project\", \"deployable project\", \"React version\",\n\"make it a Next.js app\", or \"convert to React\".\n\nThis phase converts the extracted single-file HTML clone into a deployable Next.js project.\nThe key advantage: the high-fidelity HTML (from Phase 2) serves as ground truth for the conversion.\n\n### Step 1 — Scaffold Next.js Project\n\n```bash\nnpx create-next-app@latest test_outputs/nextjs-[domain] \\\n  --typescript --tailwind --eslint --app --no-src-dir \\\n  --import-alias \"@/*\" <<< \"y\"\n```\n\n### Step 2 — Parse Clone HTML into Components\n\nUsing the topology map from Step 1.5b, split the assembled HTML into logical React components:\n\n| Topology Section | Component File |\n|-----------------|----------------|\n| `nav` / `header` | `components/Navbar.tsx` |\n| First `section` (hero) | `components/Hero.tsx` |\n| Each subsequent `section` | `components/Section[N].tsx` (or descriptive name from topology) |\n| `footer` | `components/Footer.tsx` |\n\n- `app/page.tsx` — imports and composes all components in topology order\n- `app/globals.css` — extracted CSS (the `<style>` block contents from the clone)\n\n### Step 3 — Convert HTML to JSX\n\nFor each component:\n1. `class=` → `className=`\n2. Self-closing tags: `<img>` → `<img />`, `<br>` → `<br />`, `<hr>` → `<hr />`\n3. `style=\"prop:val; prop2:val2\"` → `style={{prop: 'val', prop2: 'val2'}}`\n4. `for=` → `htmlFor=`\n5. `tabindex=` → `tabIndex=`\n6. Boolean attributes: `autoplay` → `autoPlay`, `readonly` → `readOnly`\n7. Move inline `<script>` content to `useEffect` hooks\n8. Convert `<img src=\"...\">` to Next.js `<Image>` where appropriate (optional)\n\n### Step 4 — Asset Handling\n\n- If asset download (Optional section above) was run: copy `assets-[domain]/` to `public/assets/`\n- Otherwise: keep absolute URLs pointing to original site\n- Move Google Fonts `<link>` tags to `next/font/google` imports in `app/layout.tsx`\n- Extract inline SVG icons (from Step 1.5e) to `components/icons.tsx` as React components\n\n### Step 5 — Design Tokens (if exported)\n\nIf design token export was run, convert `design-tokens-[domain].json` into:\n- Tailwind `@theme` block in `globals.css` for colors, spacing, radii\n- CSS custom properties in `:root` for remaining tokens\n\n### Step 6 — Verify Build\n\n```bash\ncd test_outputs/nextjs-[domain] && npm run build\n```\n\nIf build fails, diagnose and fix TypeScript/JSX errors. Max 3 retry attempts.\nCommon issues: unclosed tags, reserved word attributes, missing imports.\n\n### Step 7 — Report\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nNEXT.JS PROJECT CREATED\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPath            test_outputs/nextjs-[domain]/\nComponents      [N] components extracted\nAssets          [N] images, [N] SVG icons\nDesign tokens   [exported/skipped]\n\nTO RUN\n  cd test_outputs/nextjs-[domain] && npm run dev\n\nTO DEPLOY\n  vercel deploy  (or)  netlify deploy\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n```\n\n---\n\n## Key Lessons & Pitfalls\n\nThese are critical learnings from real cloning sessions. Read them before starting.\n\n### `networkidle` timeout — SPA/Next.js sites\n\nMany SPA and Next.js sites have persistent WebSocket connections, analytics pings, or\nlong-polling requests that prevent `networkidle` from ever firing. Playwright will hang\nfor 60s+ and then throw a TimeoutError.\n\n**Always use a fallback chain:**\n```python\ntry:\n    page.goto(URL, wait_until=\"networkidle\", timeout=30000)\nexcept:\n    page.goto(URL, wait_until=\"domcontentloaded\", timeout=60000)\n    page.wait_for_timeout(8000)  # manual wait for JS rendering\n```\n\nAlso: when screenshotting the local `file://` clone, use `wait_until=\"load\"` (not `networkidle`).\n\n### Large invisible overlay panels — SPA editor/chat UIs\n\nSPA sites (lovable.dev, v0.dev, etc.) often have large full-viewport invisible panels\nsitting on top of the landing page in the DOM. These are editor panels, chat UIs, or\nonboarding flows that start with `opacity: 0` and get revealed by JS interaction.\n\n**Problem:** These panels have `opacity: 0` via Tailwind's `.opacity-0` class. When Tailwind\nuses `important: true`, even `el.style.setProperty('opacity', '1', 'important')` fails to\noverride. The panel covers the hero content, making `document.elementFromPoint()` return\nthe overlay instead of the landing page text.\n\n**Detection:** Check for large (>500x500px) elements with `opacity-0` in their className.\n\n**Fix:** Remove them from the DOM before extraction:\n```javascript\nif (cls.includes('opacity-0') && el.offsetWidth > 500 && el.offsetHeight > 500) {\n    el.remove();\n    return;\n}\n```\n\nAlso add CSS fallback in the clone: `.opacity-0 { display: none !important; }`\n\n### `content-visibility: auto` — The Silent Killer\n\nMany modern sites use `content-visibility: auto` for performance. When loading from `file://`,\nthe browser never scrolls these elements into view, so they stay at 0px height forever.\n\n**Always inject:** `*, section, div { content-visibility: visible !important; }`\n\n### `overflow: hidden` on body\n\nSites often set `overflow: hidden` on body for modal management. This persists in the extracted DOM\nand prevents scrolling.\n\n**Always override:** `body, html { overflow-y: auto !important; overflow-x: hidden !important; }`\n\nNote: use `overflow-x: hidden` (not `auto`) to prevent horizontal scrollbar from baked widths.\n\n### `section { overflow: visible }` breaks hero backgrounds\n\n**Do NOT globally override** `section { overflow: visible !important }`. Many hero sections\nuse `overflow: hidden` to clip gradient background images (like lovable.dev's pulse.webp).\nSetting `overflow: visible` makes the gradient bleed out of the section.\n\n**Instead:** Only override `height` and `min-height` on sections. Use `section:first-of-type { overflow: hidden !important; }`\nto preserve hero clipping. Rely on `content-visibility: visible` (not overflow) to fix\ncollapsed sections.\n\n### Lazy-loaded images\n\nImages with `src=\"data:image/svg+xml,...\"` and `data-lazy-src=\"[real-url]\"` won't load from\na static file. The lazy-loading JS is stripped during extraction.\n\n**Always run the image fallback script** that copies `data-lazy-src` to `src`.\n\n### Cross-origin iframes\n\nIframes pointing to external domains (demo embeds, YouTube) won't load from `file://`.\n**This is expected.** They work when the clone is deployed to a web server.\n\n### Webflow animation states\n\nWebflow uses `data-w-id` attributes and JS to trigger entrance animations. The initial CSS state\nis `opacity: 0; transform: translateY(...)`. Without the Webflow runtime JS, elements stay invisible.\n\n**Always force:** `[data-w-id] { opacity: 1 !important; transform: none !important; }`\n\n### `h-screen` / `w-screen` body constraint (Next.js / Tailwind)\n\nMany Next.js + Tailwind sites set `h-screen w-screen` on `<body>`, making it exactly viewport-sized.\nThe page content overflows inside body, and scrolling happens on `document.body`, NOT on\n`document.scrollingElement` (which is `<html>`). This means:\n\n1. `window.scrollTo()` does NOT work — it controls `<html>`, which is 100vh\n2. Must use `document.body.scrollTo()` for screenshots at scroll positions\n3. In the clone fix CSS, override: `body.h-screen { height: auto !important; min-height: 100vh !important; }`\n\n**For Playwright scrolling:** always call BOTH `window.scrollTo()` and `document.body.scrollTo()`:\n```javascript\nwindow.scrollTo(0, y);\ndocument.body.scrollTo(0, y);\n```\n\n### Next.js `/_next/image` URLs\n\nNext.js optimized images use `/_next/image?url=...&w=...&q=...` paths. When extracted to a local file,\nthese become `file:///_next/image?url=...` which cannot resolve.\n\n**Fix:** Parse the `url` query parameter and prepend the original domain:\n`/_next/image?url=%2Fimages%2Ffoo.png&w=1920&q=75` → `https://domain.com/images/foo.png`\n\nAlso fix `/_next/static/media/...` paths: prepend the original domain directly.\n\n### Carousels & Sliders — THREE distinct patterns\n\nOur global `transform: none !important` fixes break carousels.\nAlways detect carousels in Step 3.5 and apply targeted fixes.\n\n**Pattern A — Infinite-scroll marquee** (logo tickers, testimonial streams):\n- Uses `@keyframes` with `translateX(-50%)` or `translateX(-33%)`, `animation: Xs linear infinite`\n- Fix: preserve the animation, keep `overflow: hidden` on parent, do NOT apply `transform: none`\n- CSS: `[class*=\"overflow-hidden\"] { overflow: hidden !important; }`\n\n**Pattern B — Stacked-grid carousel** (paginated cards, same position):\n- Multiple identical grids stacked at the same position, JS toggles visibility per page\n- All pages render at once → text overlaps\n- Fix: `parentSelector > .grid:not(:first-child) { display: none !important; }`\n\n**Pattern C — Transform-based slider** (embla, swiper):\n- Uses `transform: translateX()` to position slides, parent clips with `overflow: hidden`\n- Our `transform: none !important` collapses all slides to origin\n- Fix: NEVER apply global `transform: none`. Only target `[data-w-id]` (Webflow animations).\n\n### Typewriter / Rotating text\n\nMany hero sections have a typing prompt that cycles through example inputs via JS.\nThe static clone freezes on whatever text was visible at extraction time.\n\n**Fix:** In Step 3.6, watch the text element for ~20 seconds to capture all prompts.\nThen inject a typewriter script that cycles through them with type/delete animation.\nInclude `textarea[placeholder]` in the selector — not just `input` and `span`.\n\n### Canvas-rendered backgrounds\n\nSome sites render gradient/particle backgrounds on `<canvas>` via JS. In a static clone, canvas\nelements are blank. The visual effect disappears, exposing underlying layers (grid lines, solid colors).\n\n**Fix:** Identify the canvas parent's background color, then add a CSS `radial-gradient` simulation\nto approximate the effect. Hide empty canvas elements: `canvas.absolute { display: none !important; }`\n\n### Gradient text (`background-clip: text`) — invisible in clone\n\nMany modern sites use CSS gradient text: `background-image: linear-gradient(...);\n-webkit-background-clip: text; -webkit-text-fill-color: transparent;`. The gradient\nrenders as the text color. But in the clone, the gradient often breaks because it\nreferences CSS custom properties (`var(--brand-gradient)`) that don't resolve.\n\n**Result:** Text appears as `color: transparent` with no visible gradient = invisible text.\n\n**Fix (in Step 3):** Before extracting DOM, iterate all elements and check for\n`-webkit-background-clip: text`. If found, read the COMPUTED `background-image` value\n(which has the gradient fully resolved) and bake it as an inline style. This ensures\nthe gradient survives even when CSS custom properties are lost.\n\n### Relative URLs in Next.js / SPA sites\n\nWhen extracting DOM from a rendered page, all relative URLs become `file:///...` in the static clone.\n\n**Fix during assembly (Step 4):**\n- `/_next/image?url=%2F...` → parse the `url` param, prepend `https://domain.com`\n- `/_next/static/media/...` → prepend `https://domain.com`\n- `/images/...` → prepend `https://domain.com`\n- `/cdn-cgi/...` → prepend `https://domain.com`\n- CSS `url(/_next/...)` → prepend `https://domain.com`\n- `srcset` values — each URL in the comma-separated list needs domain prepended\n- `<source srcset=\"...\">` inside `<picture>` elements — same treatment\n\n### `overflow: visible` vs `overflow: hidden` — The Conflict\n\nThe root cause of most carousel/slider issues:\n- We need `content-visibility: visible` to fix collapsed sections\n- Carousels need `overflow: hidden` to clip off-screen slides\n- Hero sections need `overflow: hidden` to contain gradient backgrounds\n\n**Resolution:** Use `[class*=\"overflow-hidden\"]` selector to restore `overflow: hidden` on elements\nthat originally had it. This catches Tailwind's `overflow-hidden` utility class and similar patterns.\nDo NOT use a blanket `section { overflow: visible !important }` — instead use `content-visibility: visible`\n(which fixes collapsed sections without affecting overflow) and let elements keep their `overflow: hidden`.\n\n### React/Next.js + Tailwind `important: true` — The Escalation Chain\n\nSome Tailwind configs enable `important: true`, which adds `!important` to ALL utility classes.\nThis means extracted CSS overrides inline styles — even `style=\"opacity:1\"` loses to\n`.opacity-0 { opacity: 0 !important }`.\n\n**Symptoms:** DOM has correct elements, getComputedStyle shows correct values during extraction,\nbut screenshot shows blank hero. Content is there but invisible.\n\n**Solution — Escalation chain (Level 1 → 1a → 1b):**\n\n1. **Level 1a (preferred):** Keep original CSS, but remove invisible overlay panels from DOM\n   and add targeted `!important` CSS overrides. Preserves responsive layout. ~90% fidelity.\n\n2. **Level 1b (fallback):** Bake essential computed styles into inline attributes using a curated\n   property list (~60 properties). Do NOT iterate all `cs[i]` properties — Tailwind v4 sites\n   expose hundreds of CSS custom properties (`--color-purple-200`, etc.) that pollute inline styles,\n   cause color conflicts, and bloat file size to 20MB+. Use the curated list from Level 1b section.\n\n**Do NOT use the naive full-bake approach** (iterating `cs.length`). It produces 20MB+ files\nwith CSS custom property pollution that breaks colors and layout.\n\n### `section { height: auto }` kills hero `min-h-screen`\n\nMany Tailwind sites use `min-h-screen` (= `min-height: 100vh`) on the hero section so the\ngradient background fills the viewport. If you override `section { min-height: auto !important }`,\nthe hero collapses to content height and the gradient no longer fills the screen.\n\n**Fix:** In the CSS overrides, only remove `max-height` caps on sections. Do NOT override\n`height` or `min-height`:\n```css\nsection { max-height: none !important; }\n/* Do NOT add: height: auto, min-height: auto */\n```\n\n### Cookie consent banners persist in clone\n\nCookie consent / GDPR banners are extracted as part of the DOM and appear as fixed-position\noverlays in the clone. They clutter the page and cover content.\n\n**Fix:** Remove them in Step 2 (Force Visibility) by targeting common selectors:\n`[class*=\"cookie\"], [id*=\"consent\"], [class*=\"gdpr\"], [aria-label*=\"cookie\"]`\n\n### Hover/focus states captured during extraction\n\nIf the mouse cursor was over an element or an element had focus during extraction, the\ncomputed styles will include hover/focus state values (different background, shadows, etc).\n\n**Fix:** Before extraction (end of Step 1), move the mouse to corner (0,0), blur the active\nelement, and wait briefly:\n```python\npage.mouse.move(0, 0)\npage.evaluate(\"() => document.activeElement?.blur()\")\npage.wait_for_timeout(500)\n```\n\n### CSS `@layer` ordering (Tailwind v4)\n\nTailwind v4 uses `@layer base, components, utilities;` to control CSS specificity ordering.\nWhen extracting CSS via `cssRules`, the `@layer` declaration must be preserved and placed\nBEFORE any layered rules. If it appears after rules, the layer ordering is undefined and\nutilities may lose to base styles.\n\n**Fix:** When building the `<style>` block, place `@layer` declarations and `@property`\ndefinitions at the top, before all other rules.\n\n### `<html>` attributes — `data-theme`, `class`, `lang`\n\nMany sites set `data-theme=\"light\"`, `class=\"dark\"`, or similar attributes on `<html>`.\nCSS selectors like `[data-theme=\"light\"] .bg-background` depend on these attributes existing.\nIf the clone's `<html>` tag doesn't have them, those selectors fail and colors are wrong.\n\n**Fix:** In Step 4 assembly, preserve all attributes from the original `<html>` tag\n(especially `lang`, `class`, `dir`, `data-*`, `style`).\n\n### CSS custom properties (`var()`) silently fail — THE #1 fidelity killer\n\nModern sites define 100-300+ custom properties on `:root` (Tailwind v4, shadcn/ui, Radix).\nEvery color, spacing, radius, and font-size is a `var(--xxx)` reference. If even ONE `:root`\ndefinition is lost or mis-ordered, dozens of values silently fall back to browser defaults\n(usually `0`, `transparent`, or `initial`).\n\n**Symptoms:** Colors slightly wrong, spacing off, borders missing, shadows gone — hard to\ndiagnose because nothing errors, values just silently degrade.\n\n**Root cause:** In the clone, the extracted CSS has `:root` definitions inside `@layer base { }`.\nIf the `@layer` declaration order is slightly different, `base` layer has lower priority than\nexpected, and the `:root` vars get overridden by later layers with `initial` values.\n\n**Fix:** In Step 3c, extract ALL `:root` custom property values from cssRules (recursing into\n`@layer` and `@media` blocks). Then in Step 4 assembly, inject a standalone `:root { }` block\nwith all ~288 resolved values **outside any `@layer`**, ensuring they always win:\n```css\n/* Injected :root fallback — ensures all var() references resolve */\n:root {\n  --background: 45 40% 98%;\n  --foreground: 0 0% 11%;\n  --radius: .5rem;\n  /* ... all extracted vars ... */\n}\n```\n\n### `loading=\"lazy\"` images blank in `file://` context\n\nNative lazy loading (`loading=\"lazy\"`) relies on the browser's scroll-based loading. When\nopening a `file://` clone, below-fold images may never trigger loading because the browser\ndoesn't process scroll events the same way.\n\n**Fix:** Strip `loading=\"lazy\"` from all `<img>` tags in Step 3g. Images load eagerly instead.\n\n### Inner scroll containers — SPA `fixed inset-0` layout pattern\n\nMany SPA sites (wondering.app, etc.) use a layout where `<body>` has no scrollable content.\nInstead, a `position: fixed; inset: 0` wrapper contains an `overflow-y: auto` child that acts\nas the actual scroll container. All scrolling and IntersectionObserver triggers happen inside\nthis inner container, NOT on window or body.\n\n**Symptoms:** `window.scrollTo()` and `document.body.scrollTo()` have no effect. Lazy loading\nand scroll-reveal animations never trigger. The page appears as a single viewport-height frame.\n\n**Detection:** In Step 1, after initial scroll, detect inner scroll containers:\n```javascript\nconst scrollContainers = [...document.querySelectorAll(\n    '[class*=\"overflow-y-auto\"], [class*=\"overflow-auto\"], [class*=\"scrollbar\"]'\n)].filter(el => el.scrollHeight > el.clientHeight + 100);\n```\n\n**Fix:** Scroll each inner container in addition to window/body. Also, when taking Phase 3\ncomparison screenshots, scroll the inner container instead of using `window.scrollTo()`.\n\n### Behavior injection conflicts with force-visible CSS overrides\n\nStep 4 forces `[class*=\"animate-\"] { opacity: 1; transform: none; }` to make scroll-reveal\nelements visible in the static clone. But Step 4.5a injects an IntersectionObserver that\nneeds elements to START at `opacity: 0` so the animation can play.\n\n**Resolution:** When 4.5a is active, the CSS override for `[class*=\"animate-\"]` must NOT\nforce `opacity: 1` and `transform: none`. Instead, let the `.clone-reveal` class handle\ninitial state (`opacity: 0; transform: translateY(20px)`) and the `.visible` class handle\nthe reveal (`opacity: 1; transform: none`). The IntersectionObserver adds `.visible` when\nthe element scrolls into view.\n\nIf 4.5a is NOT injected (no scroll reveals detected), keep the original force-visible override.\n\n### SVG elements have no `offsetWidth` / `offsetHeight`\n\nSVG elements do not support `offsetWidth`/`offsetHeight` — these return `undefined` or `0`.\nUse `getBoundingClientRect()` instead when checking SVG visibility or dimensions.\n\n**Wrong:** `svg.offsetWidth > 0` — always fails for SVG\n**Correct:** `svg.getBoundingClientRect().width > 0`\n\nThis applies everywhere: Step 1.5e SVG detection, Phase 3 Step 5 verification, and any\ndiagnostic script that checks element visibility.\n\n### Smooth scroll library interference (Lenis, Locomotive)\n\nSites using smooth scroll libraries (Lenis, Locomotive Scroll, GSAP ScrollSmoother) wrap\nthe page in a transform-based scroll container. The extracted DOM may have\n`transform: translate3d(0, -Xpx, 0)` frozen at the extraction-time scroll position,\npushing all content off-screen in the clone.\n\n**Detection:** In Step 1.5d, check for `.lenis`, `.locomotive-scroll`, `[data-scroll-container]`,\nor custom scroll wrappers with `overflow: hidden` on body.\n\n**Fix:** If detected, reset the scroll container's transform before extraction (handled in Step 1.5d):\n```javascript\ndocument.querySelectorAll('.lenis, .locomotive-scroll, [data-scroll-container]').forEach(el => {\n    el.style.setProperty('transform', 'none', 'important');\n});\n```\n\nAlso add `scroll-behavior: smooth` to the clone's `<html>` to approximate the feel.\n\n### CSS `animate-*` classes — scroll-reveal stuck at initial state\n\nSites use CSS animation classes like `animate-cascade-drop-in`, `animate-fade-in` with initial\nstate `opacity: 0; transform: translateY(20px)`. These are triggered by IntersectionObserver\nor scroll position. In a static clone, these animations either:\n- Never start (if the trigger is JS-based)\n- Start but stay at the `from` keyframe (if `animation-fill-mode: backwards` + long delay)\n\n**Result:** Entire sections of cards/content are invisible despite being in the DOM.\n\n**Fix:** In Step 4 CSS overrides, force all animated elements to their end state:\n```css\n[class*=\"animate-\"] {\n  animation: none !important;\n  opacity: 1 !important;\n  transform: none !important;\n  display: revert !important;\n  visibility: visible !important;\n}\n```\n\nThis also catches `cascade-delay-*` classes (staggered animation delays).\n\n### Scroll-animation `transform` shifts — the 20px ghost offset\n\nMany sites use scroll-reveal animations: initial state `opacity: 0; transform: translateY(20px)`.\nOur force-visibility fixes opacity to 1, but the `translateY(20px)` remains, causing every\nanimated section to be shifted 20px down. Across 10+ sections this accumulates to ~200px.\n\n**Fix:** In Step 3f, for elements with `opacity < 0.1`, also check their `transform`. If it's\na pure translation (not rotation/scale, which carousels use), reset to `none`. Parse the\n`matrix()` values to distinguish: `matrix(1,0,0,1,X,Y)` = pure translation → safe to reset.\n\n### `@import` rules silently dropped if not first in `<style>`\n\nCSS spec requires `@import` rules to appear before any other rules in a stylesheet. If our\nassembled `<style>` block has `@font-face` or `:root` before the `@import`, the import is\nsilently ignored. This loses entire external stylesheets (Google Fonts, icon fonts).\n\n**Fix:** In Step 4 assembly, extract all `@import` rules from the CSS, place them FIRST in\nthe `<style>` block, before everything else.\n\n### `<link>` tags for fonts lost during body-only extraction\n\nWhen we extract `document.documentElement.outerHTML` and then take only the `<body>`, we lose\n`<head>` elements like `<link rel=\"stylesheet\" href=\"fonts.googleapis.com/...\">` and\n`<link rel=\"preload\" as=\"font\">`. Without these, the page renders with fallback system fonts.\n\n**Fix:** In Step 3d, separately extract font-related `<link>` tags from `<head>` and include\nthem in the output `<head>`.\n\n### Don't rewrite from scratch\n\nThe single biggest fidelity improvement came from switching from \"understand and rewrite\" to\n\"extract and clean\". The original DOM + original CSS is always more accurate than any manual\nreconstruction, no matter how carefully analyzed.","tags":["frontend","clone","skills","instantx-research","agent-skills","frontend-ui","ui-design","web-search"],"capabilities":["skill","source-instantx-research","skill-frontend-ui-clone","topic-agent-skills","topic-frontend-ui","topic-ui-design","topic-web-search"],"categories":["skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/instantX-research/skills/frontend-ui-clone","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add instantX-research/skills","source_repo":"https://github.com/instantX-research/skills","install_from":"skills.sh"}},"qualityScore":"0.455","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 11 github stars · SKILL.md body (115,213 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-24T01:03:23.949Z","embedding":null,"createdAt":"2026-04-23T13:03:56.472Z","updatedAt":"2026-04-24T01:03:23.949Z","lastSeenAt":"2026-04-24T01:03:23.949Z","tsv":"'-0':2144,2156,2178,2189 '-02':289 '-03':290 '-1':2812,2825 '-300':2429 '-5':4126 '-50':3651 '-6':4123 '/dev/null':329,335,346,357 '/matrix':2786 '/max-width':1479 '/test_outputs':235,259 '/test_outputs/.gitignore':262,265 '/tmp/clone-original-full.png':2874 '/tmp/clone-original-viewport.png':2871 '0':644,933,971,986,992,995,1000,1015,1025,1029,1031,1032,1034,1035,1069,1070,1075,1076,1078,1079,1211,1217,1248,1283,1309,1313,1314,1382,1389,1429,1455,1503,1596,1608,1614,1615,1616,1617,1687,1690,1716,1721,1734,1806,1901,1903,2061,2486,2532,2533,2721,2758,2759,2761,2762,2778,2779,2781,2803,2805,2811,3219,3228,3275,3284,3457,3462,3503,3525,3567,3644,3647,3938,3961,4032,4077,4208 '0.01':2813,2817,2821,2826 '0.1':2217,2747 '0.3':4083 '000000':145 '1':182,190,337,348,359,382,473,483,576,586,717,785,1488,1728,2220,2757,2760,2777,2780,2792,2801,2807,2816,2900,2940,3190,3265,3350,3451,3824,4079,4322 '1.5':1085,1119,1505,1626,1739,1878,2938,2946,3791 '10':642,3352 '100':967,1002,1027,1225,1688,1717,1933,1935,2428,3648 '1000':1038 '11':2534 '12':155,159 '120s':3733,3773 '1440':606,809 '150':1001,1026 '1a':393,419,487,499,577,655,665,677 '1b':407,503,578,672 '2':229,328,336,347,358,389,401,415,422,429,439,449,459,518,710,973,2016,2725,2820,2976,3286,3863 '200':3486,3488 '2000':2326 '20px':2723,2733 '21bbd1bc18f6137e':3690,3727 '3':134,138,293,432,533,1527,2328,2824,3002,3473,4125 '3.4':2880 '3.5':3135,3147,3777 '3.6':4094 '30':1390,3962,4113,4197 '300':3398,4040 '3000':619,1042,3939 '30000':834 '3a':2336 '3b':2380 '3c':2412 '3d':2542 '3e':2618 '3f':2704 '3g':2831 '4':367,442,546,1536,2285,2540,3141,3505,3598,4092,4122,4311 '40':1284,1310,1383,1735,1807,2529 '400':627,990,1019,3970 '45':2528 '5':452,565,1504,3532,4226 '50':456 '500':1083,2191,2193,3862,4218 '500ms':4200 '60':1249,1430,1597,3276,3285,3458,3463,3504,3526,4199,4210 '600':1215,3396 '60000':843 '70':446 '720':626 '80':412,436,3220,3568 '8000':847 '85':426 '90':398 '900':608,811 '97':386,722 '98':2530 'absolut':1707,2281 'accident':253 'accordion':1404,1574,2995,3189,3506,3512,3784 'accordion/details':3828 'across':3694 'activ':1059,3920 'actual':905,2656,3761 'add':661,3636 'alongsid':4145 'also':2042,2175,3231 'alway':93,3750 'anim':745,926,1127,1144,1161,1172,1991,2030,2183,2211,2709,2716,2970,3196,3223,3319,3544,3569,3653,3656,3731,3765,3771,4011,4068 'animate-infinit':3543 'animate-infinite-scrol':3652,3764 'animation-candid':1160 'animation-hidden':2210,2708 'animation-timelin':2969 'announc':690,1980,3098 'answer':113 'ao':1175 'api':321,791 'app':4298 'appear':2951 'append':288 'appl':2611 'apple-touch-icon':2610 'appli':3714 'argument':97,194 'aria':2308 'aria-label':2307 'array.from':3257,4230 'articl':2200 'artifact':250 'ask':214 'askuserquest':216 'assembl':479,510,527,584,1889,2286,2541,4312 'asset':1629,1652,1969,1971 'async':935,1150,3880,4172 'auto':580,597,901,956,960,2028,2320,2322,3095,3128,3857 'auto-carousel':3094,3127 'auto-escal':579,596 'autoplay':1783 'avail':295,379,467 'avoid':1054,1061 'await':997,1022,1036,1219,3964,4034,4212 'awar':1887 'b':430,1498,1500,1506,2802,2939,3334,3778,3876 'b-a':1499 'back':1202,4030 'background':1637,1672,2527,2631 'background-clip':2630 'backgroundcolor':3493 'backgroundimag':1677 'backgrounds/effects':3479 'bad':3710 'bake':411,504,675,2663,2858 'banner':1553,1661,2291 'base':1531 'bash':255,311 'becom':872 'before.foreach':1231 'before.set':1188 'behavior':1121,1148,1326,1397,1951,1953,1955,1957,1959,1961,2943,2985,3054,3075,3788,3823 'best':761 'better':128 'bg':1611,1684 'bgclip':2676,2683 'bgimag':2688,2691,2692,2695 'blank':485,651,668 'block':824,2456,2506,3749,3853 'blur':1058,1073 'bodi':880,977 'body/html':2318 'bottom':1200 'boundari':1516 'box':3303,3405,4183 'branch':713 'break':1384,2437,3171,3738 'breakpoint':1136,1448,1457,1494,1495,1999 'breakpoints.add':1485 'broken':501 'brows':356,361,364,530,700 'browser':34,736,800 'browser.close':2878 'browser.new':602,805 'btn':1363 'build':137,2906,4317 'button':1357,1359 'c':318,440,601,1627,1811,2804,3260,3468,3469,3483 'c.classname':3261 'c.getcontext':1814,1816 'c.height':3498 'c.offsetheight':3487 'c.offsetwidth':3485 'c.parentelement':3492 'c.parentelement.classname':3501 'c.width':3496 'candid':1162 'canva':1698,1750,1809,3474,3481 'canvas':3188 'captur':1055,1062,2654,3369,3798,3867,3899,4007,4095,4107,4116,4134,4202 'capturestyl':3909,3951,3973 'card':1361 'caret':3441 'caret-transpar':3440 'carousel':1572,3096,3129,3186,3191,3201,3237 'carousel/rotation':2770 'cascad':2464 'catalog':1883 'catch':865,1353,1385,1489,1820,2364,2408,2519 'caus':2732 'cb':3071 'cb.get':3068,3077,3081 'chain':582 'chang':1228,3292,3324,3984,4105 'charact':3328,3330 'character-by-charact':3327 'chat':2055,2133 'check':299,312,592,696,1226,1668,1691,1745,2941,2983,3029,3346 'child':902 'children':1694,3225,3245,3577 'childrenperpag':3227 'chromium':735 'class':952,957,961,1171,1182,1184,1260,1262,1360,1362,1403,1405,1407,1416,1571,1573,1575,1579,1581,1658,1660,1662,1791,1834,1868,2293,2295,2297,2305,3200,3202,3204,3206,3243,3248,3383,3385,3387,3402,3406,3408,3418,3511,3513,3542,3546,3548,3550,3668,3672,3685,3754,3888,3921,4180,4185,4190 'click':1131,1391,1396,1958,1960,2908,2984,3000,3074,3086,3114,3787,3822,3864,4027,4033 'click-driven':1130,2999,3085,3113 'click-tab':2907 'clip':2632 'clone':4,157,227,267,590,1539,2440,2653,2905,3175,3632,3808,4071 'cloner':9,60 'close':3843 'cls':1244,1279,1305,1378,1425,1592,1802,1831,1835,1838,1840,2084,3045,3048,3069,3105,3112,3123,3215,3271,3521,3563 'cls.includes':2187 'cls.tolowercase':2089 'clslow':2088 'clslow.includes':2121 'code':90 'col':3251 'collaps':1406,3507,3795,3827 'collis':284 'color':2638,3445 'column':136,140 'commit':254 'comparison':1532,2868 'complet':749,2922,4146,4163,4237,4291 'compon':1133,1893,2892,3701 'composit':1636 'comput':42,506,2657 'connect':822 'consent':2289,2296,2302 'const':623,632,636,936,949,978,1008,1151,1166,1203,1234,1266,1295,1328,1330,1335,1341,1367,1399,1456,1461,1467,1476,1544,1561,1564,1568,1654,1666,1673,1701,1761,1904,1916,1919,1925,2079,2083,2087,2090,2103,2194,2249,2344,2347,2352,2355,2388,2391,2397,2469,2471,2475,2490,2512,2565,2572,2576,2671,2675,2679,2687,2740,2748,2783,2789,3184,3210,3254,3391,3399,3413,3429,3489,3556,3881,3883,3908,3910,3931,3949,3953,3957,3971,4173,4177,4221 'contain':22,884,907,912,948,1006,1291,1665,1669,1676,1730,1867,2169,3304,3763,3809,4289,4315 'container.classname':1731 'container.queryselectorall':1695 'containerbg':1674,1679,1680 'containerbg.slice':1686 'content':774,2026,2041,2067,2149,2172,3796,3811,3819,3869,3878,3924,4044,4050,4324 'content-vis':2025 'contentinfo':1557 'context':2839 'continu':3028 'cooki':2288,2294,2300,2310 'cookiebann':2306 'core':98 'corner':1053 'correct':2560 'count':1446,1492 'cover':2063,2146 'creat':236,242 'creativ':101 'critic':2419,3156,3665 'cross':2367 'cross-origin':2366 'cs':637,1565,1702,2080,2672,2741,3211,3557,3932 'cs.animationduration':3572 'cs.animationname':3224,3570 'cs.backgroundclip':2678 'cs.backgroundcolor':1612,1618 'cs.backgroundimage':2689 'cs.display':645,3574,3946 'cs.opacity':643,2216,2746,3942 'cs.position':1610,1706,1708 'cs.transform':2750,3944 'cs.visibility':3948 'cs.webkitbackgroundclip':2677 'cs.webkittextfillcolor':2681 'cs.zindex':1607,1720 'css':43,385,396,424,434,444,478,490,514,524,539,555,658,720,754,1322,2330,2416,2626,2641,2926,3615,3634,3641,3679,3709,3756,3871,3981,4051,4061,4088 'css/html/js':92 'cssdiff':3987,4073 'cssrule.font':2403 'cssrule.media':1473 'cta':1663 'custom':2332,2417,2430,2642 'cycl':4119,4121,4204 'd':450,1481,1740,2806,3836 'd.setattribute':3837 'dark':4295 'dashboard':4306 'data':1174,1177,1180,1414,1437,1837,1865,2227,2231,2235,2240,2245,2253,2258,2264,2992,3119,3841,3848,3893,3918 'data-ao':1173 'data-lazy-src':2226,2239,2252 'data-nimg':2234 'data-s':1179 'data-scrol':1176 'data-scroll-contain':1864 'data-src':2230,2244,2257 'data-st':1413,1436,2991,3118,3840,3847,3917 'data-tab':3892 'decis':111 'default':3815,3902,4025 'defin':2427 'definit':3611,3627 'delay':937,998,1023,1037 'delet':3332 'descend':4247 'design':109 'detail':1402,1583,3515,3832 'detect':185,296,550,599,945,1140,1254,1287,1319,1393,1521,1630,1631,1746,1847,1882,2887,3138,3148,3232,3372,3420,3604,4101 'determin':187,306,2883,2893,3009,3131 'diff':3872,3982,4053,4062 'direct':36 'directori':233,244 'discov':1123 'display':3168,3573,3662,3728,3748,3768,3852,3945 'div':3744 'document.activeelement':1072 'document.body':4021 'document.body.scrollheight':981,1206 'document.body.scrollto':994,1033,1077 'document.body.style.overflow':2319 'document.documentelement.outerhtml':2862 'document.documentelement.scrollheight':982,1207 'document.documentelement.style.overflow':2321 'document.elementfrompoint':625 'document.queryselector':1833,1836,3912,4016 'document.queryselectorall':951,1170,1257,1292,1355,1401,1546,1656,1769,1787,1808,1859,1908,2076,2237,2271,2292,2567,2668,2737,2841,3199,3247,3380,3480,3510,3541,3831,3839,3885,4179 'document.stylesheets':1338,1464,2350,2394,2515 'dom':40,384,476,536,719,751,780,1096,2331,2624,2852,3160,3803 'domain':208,268,270 'domcontentload':816,841 'dotlotti':1794 'dotlottie-play':1793 'download':425,435,522,538 'driven':1132,2034,2916,2959,2982,3001,3042,3060,3065,3087,3093,3108,3115,3126,3151 'dropdown':2102,2109 'dropdown-list':2108 'durat':3571,3658 'e':460,1354,1386,1490,1821,1879,2365,2409,2520 'e.g':277,3305,3361,3623,3687,3741,4293 'eas':4085 'echo':263,330,338,341,349,352,360,363 'edit':464,758 'editor':2053,2131 'effect':1129 'el':624,629,639,964,1187,1189,1192,1196,1233,1237,1265,1269,1294,1298,1365,1421,1559,1567,1700,1704,1797,1873,2078,2082,2312,2670,2674,2739,2743,3209,3213,3390,3444,3517,3555,3559,3845 'el.children.length':3226,3578 'el.classlist.contains':3530 'el.classname':1245,1280,1306,1379,1426,1593,1803,2085,3216,3433,3438,3522,3564 'el.clientheight':966 'el.closest':3401 'el.getattribute':1432,1435,1800 'el.getboundingclientrect':1563,3393 'el.hasattribute':1440,3528 'el.id':1590,2092 'el.matches':1373 'el.offsetheight':1725,2192 'el.offsetwidth':1723,2190,3222,3397,3576 'el.parentelement':3412 'el.queryselector':1570,2196 'el.queryselectorall':3580,3584 'el.remove':2205,2313 'el.scrollheight':965 'el.setattribute':3846 'el.src':1714 'el.style.backgroundclip':2698 'el.style.backgroundimage':2694 'el.style.color':2702 'el.style.setproperty':1874,2218,2827,3851,3855 'el.style.webkitbackgroundclip':2696 'el.style.webkittextfillcolor':2700 'el.tagname':1278,1377,1424,1588,3431,3520 'el.tagname.tolowercase':1712 'el.textcontent':634 'element':866,1060,1146,1163,1256,1320,1395,1994,2095,2213,2270,2622,2711,3153,3162,3193,3336,3475,3697,3717,3740,3829,3907 'elif':3066 'els':2594,2602,3088 'embla':3205 'empti':1649 'english':94 'ensur':245,2457,3625,3806 'enter':310 'entir':850,1646 'equal':135,139 'escal':559,581,598,652,669 'essenti':505 'etc':291,889,2135 'eval':537 'everi':110 'exact':76,149 'except':835 'execut':783 'exist':78,248,287,4282 'existing.startswith':4279 'expand':3793 'expandable/toggleable/tab':1394 'expect':706 'expens':2902,4309 'explicit':3007,3638 'extern':523 'extract':37,191,207,475,554,712,721,747,1049,1088,1756,1853,1894,2073,2329,2337,2381,2413,2446,2543,2553,2625,2849,2889,2960,3155,3159,3606,3782,3804,4323 'f':260,610 'face':2384,2404 'fade':1185 'fail':420,2443 'faith':13,65 'fallback':814,2454 'fals':631,1766 'faq':3514 'faq/toggle':3508 'favicon':2584 'feed':3132 'fetch':303,548 'ffffff':147 'fidel':387,399,413,427,437,447,457,707,723,2421 'file':24,286,466,525,760,4320 'filenam':210,266 'fill':2637,3229 'filter':963,3259,3467,3992,4249 'final':3802 'find':1113,1982,3591 'fire':909 'firecrawl':334,339,342,423,515,519,698 'first':300,858,2944,4028 'fix':397,681,893,1263,1275,1709,2222,2267,2279,2726,2857,2927 'fixed-posit':892 'flex':3663,3729,3769 'flow':2058 'focus':1063 'font':129,2334,2383,2386,2389,2411,2546,2557,2580,2582,2599 'font-fac':2382 'fonts.googleapis':2589 'fonts.gstatic':2591 'fonts.push':2406 'footer':1550 'forc':2017,2207 'foreach':1186,1264,1293,1364,1420,1558,1664,1699,1771,1796,1810,1830,1872,1913,2077,2247,2275,2311,2570,2669,2738,2845,3208,3252,3389,3482,3516,3554,3835,3844 'foreground':1638,2531 'format':2775 'found':202,213,1152,1316,1329,1400,1443,1762,1842 'found.lotties.push':1798 'found.push':1375,1422 'found.scroll_libs.push':1839 'found.scroll_reveals.push':1243 'found.scroll_snap.push':1304 'found.slice':1388 'found.sticky.push':1276 'found.videos.push':1773 'found.webgl':1818 'fragment':4139 'free':173,177 'frontend':2 'frontend-ui-clon':1 'frozen':1849 'full':409,2139,2850,2875,3366,4118,4203 'full-screen':2138 'function':3753 'g':3253 'g.children.length':3280 'g.classname':3262,3272 'g.parentelement':3256 'g.parentelement.children':3258 'g.parentelement.classname':3282 'gate':2885 'gdpr':2298,2304 'generat':1510,3683 'generic':3621 'get':171,175,3055 'getcomputedstyl':638,1191,1195,1236,1268,1297,1566,1675,1703,2081,2673,2742,3212,3443,3491,3558,3933,4015 'gitignor':247 'given':10 'good':3757 'gradient':2620,2628,2646,2659 'gradient-imag':2658 'gradient-text':2627 'grid':3236,3244,3250,3270 'grid-col':3249 'gstack':345,350,353,528,699 'gstack/browse':433 'h':979,988,1204,1213,1602,1724,1781,1926,1934,3497 'h1':2197 'h2':2198 'h3':2199 'handl':47 'hascont':2195,2204 'hasinteract':1569,1621 'head':2170,2562,2568,3743 'header':1259,1548 'headless':534,802 'height':607,810,1929,3856 'hero':484,593,620,649,666,1659 'hidden':2099,2212,2316,2710,3916,3926,4020 'hide':2037 'highest':378 'hostnam':271 'hover':1056,1128,1317,1323,1325,1348,1954,1956,1993 'hover/g':1351 'hoverselector':1331,1370 'hoverselectors.add':1349 'href':2577 'href.includes':2588,2590 'html':23,49,269,465,480,511,549,759,779,2860,3936,3978,4049,4316 'html/rawhtml/markdown':521 'http':547 'hybrid':395,656 'icon':1881,2014,2605,2608,2613 'icons':1902,1936,1942 'id':1589,2091,2299,2301,2303 'id.includes':2123 'identifi':3161 'idx':1585 'imag':1635,1643,2225,2660 'img':1696,1911,2238,2243,2248,2842,2846,3581 'img.getattribute':2251,2256 'img.removeattribute':2847 'img.src':2262,2265 'img.src.startswith':2263 'imgcount':3579 'import':322,403,495,684,792,1877,2152,2221,2371,2375,2830,3343,3661,3664,3730,3770,3854,3858 'improv':71 'includ':3434,3439,3629,4045 'index':1519 'infinit':3535,3545,3552,3654,3660,3735,3766,3775 'infinite-scrol':3534,3551 'info':570,1451,1897,1963,1965,1977,1979 'initi':930,2184,2718 'inject':2451,2537,3145,3180,3600,3705,4086 'inlin':509,1884,2159,2665 'inner':882,946,1005 'input':184,3302,3376,3381,3384,3404,3411,3423,3432,4182,4188 'input-box':3301,3403,4181 'insid':3299 'insight':4130 'instead':3364,3371,3755 'inter':123,125 'interact':1522,1620,1622,2881,2891,2895,2932,2933,3005,3010,3014,3023,3024,3101,3152,3182,4167 'intersect':861 'intersectionobserv':908,2963,3109 'interv':3342 'invis':492,2020,2050,2128 'issu':2465 'istransparentinput':3430,3452,3472 'itemsperpag':3279 'job':63 'join':3470 'js':51,782,2033,2973,3150,3166,3326,3477 'js-driven':2032,3149 'js-render':50,3476 'jsx':3678,3689,3712,3726 'jsx-21bbd1bc18f6137e':3688,3725 'jsx-xxx':3711 'k':2120,2122,2124,3122,3993,3996,3999,4001,4004 'keep':488,657,2369,2556,2579,4152,4235 'keephidden':2104 'keephidden.some':2119 'key':3906,4129,4266 'keyfram':3608,3618,3642 'label':2309,3003,3958,3977 'land':2065,2147,3388,3410,4187 'landing-input':3409,4186 'languag':79,85 'larg':2049,2127 'last':416 'later':1117,2867 'layer':1628,1634,1651,1667,1736,1968,1970,2001,2435,2503 'layers.length':1727 'layers.push':1682,1710 'layout':689 'lazi':742,856,2223,2228,2241,2254,2834,2844 'left':2729 'len':4267 'length':3345,3450,3582,3586,4246 'leni':1822,1860 'lenient':3349 'let':969,984,1013,1209,1453,1899,2484,4206 'level':371,380,381,392,406,421,431,441,451,462,469,472,482,486,498,502,517,532,545,564,575,585,654,664,671,676,702,715,716 'lib':1768,1857,2008 'liberti':102 'librari':1742,1754,1846 'like':3708,4140 'linear':278,3659,3734,3774 'linear.app':280 'link':2558,2563,2566,2569,2571,2617 'link.as':2598 'link.href':2578 'link.outerhtml':2593,2601,2615 'link.rel':2574 'links.push':2592,2600,2614 'list':2110 'listen':2975 'load':615,743,857,869,2224,2833,2843,2848 'locomot':1824,1862 'locomotive-scrol':1823,1861 'logo':3537 'longer':4259 'longest':4155,4239 'look':1648 'loop':1785 'lost':2550 'lotti':1748,1764,1789,1792,2005 'lottie-play':1788 'm':1477,1483,1487,2784,2788,2791,3111 'magicpath':3306 'main':1551,1555,2202 'make':1644 'mandatori':2884 'mani':817,885,2022,2712 'manual':45,526,571 'map':1169,1509,1513,1988,2794,3998 'mark':2931 'marque':3533,3547,3605,3724,3762 'match':116,4087 'math.abs':2809,2814,2818,2822 'math.max':980,1205 'math.round':1599,1603 'matrix':2756,2774,2776 'may':2174,2648,3356 'mechan':2962 'media':1447,1741,1758,1972,1974,2504 'media_precheck.get':1855 'mediarul':1454,1484,1493 'menu':2113 'merg':165 'method':470,704,725 'mid':3359,4136 'mid-typ':3358,4135 'mirror':81 'miss':1640 'mistak':2903 'mkdir':239,256 'mobil':2112,4297 'mobile-menu':2111 'modal':2100,2105 'mode':4296 'model':1523,2882,2896,3011,3026,3062,3084,3090,3102 'modern':2422 'modul':3680 'mous':1051 'move':1050 'ms':938,944 'multi':1633,3234,3780 'multi-lay':1632 'multi-pag':3233 'multi-st':3779 'multipl':3240,3695 'must':112,2069,3347 'mutat':1094 'n':703,1986,1989,1992,1995,1997,2000,2004,2006,2011,2013,2363,3104 'name':705,1515,3609,3622,3673,3686 'name/none':2009 'natur':1109 'nav':1258,1547,2116 'navig':812 'need':238,3177 'neighbor':871 'networkidl':815,825,832 'neutral':1046 'new':939,1168,1220,1332,1458,1906,3965,4035,4175,4213 'next.js':767,1892,2045,2233 'nimg':2236 'non':89 'non-cod':88 'none':162,646,1303,1681,1876,2693,2754,2829,4023 'noth':562 'notif':2290 'now.opacity':1240,1253 'now.transform':1242 'null':1591,1619,3930 'object.entries':3990 'object.fromentries':3989 'observ':862,1104 'off-screen':2038 'often':2047 'ok':327,340,351,362 'onboard':2057 'one':4260 'opac':932,1190,1193,2060,2143,2155,2177,2188,2208,2219,2720,2727,3941,4075,4082 'open':1439,1441,3527,3529,3531,3825,3834,3838,3850 'order':161,375,2436 'origin':118,121,132,143,153,168,489,513,2368,2912 'output':230,232,463,587,611,755,4319 'overflow':899,954,959,2315 'overflow-auto':958 'overflow-y-auto':898,953 'overlap':3035 'overlay':493,660,1639,1642,2021,2051,2101,2106,2117,2129,3297 'overlayspan':3414,3448 'overlayspan.classname':3465 'overlayspan.classname.split':3466 'overlayspan.textcontent.trim':3449,3455 'overrid':496,662,2161 'p':240,257,799,4272,4280,4286 'p.chromium.launch':801 'page':16,30,600,603,711,731,804,806,851,1044,1106,1507,2066,2148,2876,3235,3277,3614 'page.evaluate':934,1071,1149,1327,1398,1452,1543,1653,1760,1858,1898,2075,2343,2387,2468,2564,2667,2736,2840,2861,3183,3830,3879,4171 'page.goto':827,836 'page.mouse.move':1068 'page.screenshot':2869,2872 'page.wait':844,1039,1080,2323,3859 'page_c.close':647 'page_c.evaluate':622 'page_c.goto':609 'page_c.wait':616 'panel':2052,2054,2132,2141,3868,3882,3898,3911,3928,3934,3976,4042 'panel.innerhtml.slice':3937 'parallax':1158 'parallel':316 'parent':3238,3400,3415 'parent.classname':3460 'parentbg':3490,3499 'parentcl':3281,3459,3500 'pars':183,193 'parsefloat':2215,2745,2796 'parseint':1486,1606,1719,1921,1927 'pass':859,864,970,972,974,999,1024 'path':231,612,2870,2873 'pattern':3295,3333 'per':4047 'per-tab':4046 'perfect':7,58 'perplex':3307 'persist':821 'phase':181,308,388,400,414,428,438,448,458,709,1526,1535 'photocopi':106 'pictur':2272 'pixel':6,57,4143 'pixel-perfect':5,56 'placehold':4194 'play':4300 'player':1790,1795 'playwright':26,324,326,331,383,394,408,471,481,497,516,697,718,794,797 'playwright.sync':320,790 'pleas':217 'popup':2107 'pos':1267,1272,1274,1286 'posit':894,1270,1285,1609,1693,2734,2967 'post':4149,4233 'post-process':4148,4232 'potenti':1891 'pre':1087,1744 'pre-check':1743 'pre-extract':1086 'precheck':1759,1973,1975 'prefer':680 'prefix':4159,4243,4256 'preload':2547,2583,2597 'present':776 'preserv':687 'prev':1232 'prev.opacity':1239,1251 'prev.transform':1241 'primari':724 'principl':99 'print':325 'prioriti':374 'proceed':694,1984,3100 'process':4150,4234 'produc':4138 'progress':874 'promis':940,1221,3966,4036,4214 'prompt':3367,3386,4097,4110,4124,4147,4164,4170,4191,4238,4262,4265,4270,4275,4284,4287 'prompts.append':4285 'prop':2491,2497,2499 'prop.startswith':2495 'properti':2333,2418,2431,2643,4009,4054 'provid':218 'pure':2798 'pwd':234,258,261,264 'python':595,788,1137,1541,1650,1757,1895,2074,2335,3008,3181,3817,4165 'python3':317 'queryselector':3416 'r':941,943,1222,1224,3967,3969,4037,4039,4215,4217 'rang':3040 'raw':4169,4264 're':3363 'react':2046 'read':1101 'read-on':1100 'real':33,734,2171,2250,2261,2266 'rebuild':556 'recon':1947,1949,1981,1985,3019,3052,3073 'recon.get':3821 'reconnaiss':1089,1092 'reconstruct':445,540,572 'record':3589,3980,4302 'rect':1562,1917,3392 'rect.height':1604,1930 'rect.top':1600,3395 'rect.width':1924 'recurs':2501 'redesign':70 'refer':2442,2460,2864 'regardless':2462 'reinterpret':73 'rel':2573,2586,2596,2604,2606,2609 'reli':3164 'remov':491,659,2019,2071,2126,2164,2287,2314,2832 'render':28,39,52,474,729,750,786,2561,2851,3478 'replac':3178,3601 'report':1540 'reproduc':14,75,4064 'reproduct':66,4012 'request':96 'requir':2920 'research':454,574 'reset':1043,1848,2705,2764 'resolv':2448,2650 'resort':417 'respons':688,1135,1444,1450,1962,1964,1998 'restor':3639,4024 'result':305,1948,3185,3588 'result.accordions.push':3518 'result.canvases.push':3494 'result.carousels.push':3214,3266 'result.marquees':3560,3561 'result.marquees.push':3562 'result.typewriters.push':3453 'retro':4142 'return':630,640,1315,1387,1442,1491,1624,1737,1841,1940,2125,2206,2378,2410,2521,2616,3587,3897,3929,3935,4041,4229 'reveal':875,925,1154,1183,2035,2715,2954,3034,3057 'revers':4268 'rewrit':46,2923 'rgba':1613 'role':1358,1409,1411,1431,1433,1552,1554,1556,1577,2201,2989,3078,3886,3913,4017 'root':2415,2433,2455,2466,2482,2523 'rotat':3288,4109 'rule':80,1324,1342,1449,1468,1474,2353,2356,2385,2398,2405,2473,2476,2478,3640,3707 'rule.conditiontext':1475 'rule.conditiontext.match':1478 'rule.cssrules':2508,2510 'rule.csstext':2360,2407 'rule.selectortext':1346,2480 'rule.selectortext.includes':1347,2481 'rule.selectortext.replace':1350 'rule.style':2492 'rule.style.getpropertyvalue':2498 'rule.style.length':2488 'rule.type':1472,2402 'rules.join':2362 'rules.push':2359 'run':297,1090 's.textcontent.trim':4223 'safeti':2162 'sal':1181 'same-class':3241 'say':170 'sc':1009 'sc.scrollheight':1017 'sc.scrolltop':1020,1028 'scope':3684,3702,3720 'scoped/generated':3671 'scrape':520 'screen':2040,2140 'screenshot':535,542,588,2865 'screenshots/design':568 'script':3143,3179,3602 'scroll':737,787,848,876,883,906,920,924,947,975,1003,1065,1125,1138,1142,1147,1153,1156,1165,1178,1198,1230,1289,1753,1767,1825,1828,1845,1850,1856,1863,1866,1871,1950,1952,1990,2007,2182,2714,2915,2942,2953,2958,2965,2974,2981,3033,3041,3053,3056,3059,3064,3107,3536,3553,3624,3643,3655,3657,3732,3767,3772 'scroll-anim':2181 'scroll-driven':2914,2957,2980,3063,3106 'scroll-rev':923,2713,3032 'scroll-snap':1288,2964 'scroll-trigg':1124,1141 'scroll/slide':3195 'scrollbar':962 'scrollcontain':950,1011 'scrollsnaptyp':1299 'search':3407 'second':863,4114,4198 'section':156,1512,1530,1545,1549,1625,1647,1657,1987,2173,2930,2950,3006,3015,3017,3022,3025,3037,3061,3083,3089,3103,3110,3121,3509 'section-bas':1529 'section.get':3047 'sections.push':1584 'seen':4174,4231 'seen.add':4227 'sel':1368,1374 'select':369 'selector':3669 'self':21,4314 'self-contain':20,4313 'sentenc':4292 'set':1333,1459,1907,2717,4176 'settimeout':942,1223,3968,4038,4216 'shadcn/ui':2426 'share':3693 'sheet':1336,1462,2345,2348,2379,2392,2513 'sheet.cssrules':1344,1470,2358,2400,2518 'sheet.href':2373,2377 'sheets.push':2361,2374 'shift':2735 'shortcut':2607 'show':3318,4074 'sibl':3255,3377,3427,3739 'siblings.length':3264,3278 'silent':2444 'simplebar':1829 'singl':19 'sit':3312 'site':391,405,686,728,764,772,819,887,2023,2044,2154,2423,3674 'six':370 'skill' 'skill-frontend-ui-clone' 'skip':163,2094 'slice':1247,1282,1308,1381,1428,1502,1595,1715,1733,1805,3218,3274,3283,3456,3461,3502,3524,3566,3960 'slider':1582,3192,3207 'smooth':1752,1827,1844,1870 'smooth-scrol':1826,1869 'snap':1157,1290,1296,1301,1302,1311,2966 'snapshot':1159,2619 'sort':1496,4244,4261,4263,4274 'sourc':1777,2273 'source-instantx-research' 'spa':551,558,886,2043,2130 'spa/next.js':818 'span':3298,3322,3355,3378,3417,3428,3471,4178,4184,4189,4192 'spans.foreach':4219 'spanselector':3464 'specif':3667 'split':2793 'sr':3050 'sr.get':3044 'src':1713,1774,1778,1799,1801,2229,2232,2242,2246,2255,2259,2276 'srcset':2268,2274 'ssr':770 'ssr/ssg':768 'st':4144 'stack':1655,1738,2002,3246,3269 'stacked-grid':3268 'stacks.push':1729 'standalon':3335 'start':172,176,179,198 'state':931,1047,1057,1064,1110,1415,1434,1438,2185,2719,2993,3082,3120,3781,3800,3816,3842,3849,3873,3875,3900,3919,3986,4026,4052 'statea':3950,3988 'statea.styles':3995,4003 'stateb':3972,3975 'stateb.html':3979 'stateb.styles':3991,4014 'static':48,553,763,1623,3027,3174 'static/ssr/webflow':390 'stay':927,2098 'step':189,228,292,366,784,1084,1098,1118,2015,2284,2327,2539,2724,2879,2937,2945,3134,3140,3146,3157,3597,3776,3790,4091,4093,4310 'sticki':1145,1155,1261,1273,2968 'sticky/fixed':1255 'still':500,667 'store':203,1112,1945 'strategi':188,368,692,701 'strict':373 'string':4252 'strip':3538,4325 'stripe':281 'stripe.com':283 'structur':3374,3422 'style':410,507,674,2160,2342,2666,2767,3677,3904,3940,4305 'styled-jsx':3676 'stylesheet':2339,2581,2587 'support':2505 'svg':1880,1896,1909,1912,1914,1976,1978,3585 'svg.getattribute':1922,1928,1938 'svg.getboundingclientrect':1918 'svgcount':3583 'svgs':1885,2010 'swap':3340 'sweep':1122,1139,1318,1392,1445 'swiper':1580,3203 'swiper/embla':3198 'swiss':4304 'switch':4067 'sync':323,793,796 'system':569 't.length':4225 't.match':2785 'tab':1410,1418,1576,2909,2990,3079,3116,3783,3799,3818,3866,3877,3884,3887,3890,3894,3903,3923,3954,3956,4029,4031,4043,4048,4066 'tab-cont':3922 'tab-switch':4065 'tab-trigg':1417,3889 'tab.click':3963 'tab.textcontent.trim':3959 'tablist':1412,1578 'tabpanel':3914,4018 'tabs.length':3896 'tabs/accordions':1996 'tag':1277,1376,1423,1587,2544,3519 'tailwind':402,683,2151,2424,3682 'take':2863 'tape':3540 'target':205,494,673,828,837,878,3751,3759 'text':91,169,633,2341,2621,2629,2633,2636,2684,2697,2699,3289,3290,3311,3320,3323,3338,3344,3419,3426,3436,3454,4104,4132 'text-transpar':3435 'text.length':641 'textarea':3382,4193 'textfil':2680,2685 'three':877 'ticker':3539,3549 'time':3092,3125,3294,3370,4059 'time-driven':3091,3124 'timelin':2971 'timeout':618,833,842,846,1041,1082,2325,3861 'tld':276 'toggl':1408,3785 'tolowercas':2093,2575 'tool':186,294,314,468,695 'top':1067,3314 'topic-agent-skills' 'topic-frontend-ui' 'topic-ui-design' 'topic-web-search' 'topolog':1508,1542,1966,1967,2936,3020 'tostr':1246,1281,1307,1380,1427,1594,1732,1804,2086,3217,3273,3523,3565 'total':1900,1915,1941,2012 'touch':2612 'track':4308 'transform':1194,1197,1851,1875,2706,2731,2768,2772,2828,3645,3649,3943 'transit':4008,4013,4022,4058,4081,4089 'translat':2799 'translatex':3646,3650 'translatey':2722,2766,2771 'translatey-styl':2765 'transpar':2639,2686,2701,2703,3310,3375,3437,3442,3446 'transparent/caret-transparent':3425 'tri':826,1339,1371,1465,1812,2351,2395,2516 'trigger':740,854,860,1126,1143,1419,2837,2996,3891 'trim':635,1352,2500 'true':404,685,803,1819,2153,2877,4269 'twice':852 'type':1683,1711,3267,3331,3360,4137 'typewrit':3097,3187,3287,3379,4096,4099,4131,4168 'typic':4120 'ui':3,2056,2134 'uniqu':1943 'unrel':3696 'url':12,192,197,201,206,212,220,829,838,1685,2282,2376 'use':25,122,124,144,148,376,890,1115,1524,2024,2176,2640,3595,3619,3666,3675,3786,4060 'user':83,95 'v':1772,2795,3994,3997,4000,4006 'v.autoplay':1784 'v.loop':1786 'v.offsetheight':1782 'v.offsetwidth':1780 'v.queryselector':1776 'v.src':1775 'v.trim':2797 'v4':2425 'val':2790,2810,2815,2819,2823 'valu':2449,2661 'var':2441,2459,2467,2470,2496,2522,2524 'versa':2919 'version':4156,4240 'via':3325 'vice':2918 'video':1697,1747,1763,1770,2003 'viewbox':1905,1939 'viewboxes.add':1937 'viewboxes.size':1944 'viewport':604,807 'vinyl':4301 'visibl':594,621,873,2018,2027,3947 'vue/nuxt':769 'w':1722,1779,1920,1932,2115,3221,3495,3575 'w-nav-overlay':2114 'wait':613,830,839 'walk':2472,2509,2517 'want':225 'watch':4102,4111,4195 'webfetch':443,544 'webflow':765,2029 'webgl':1749,1765,1815 'webgl2':1817 'webkit':2635 'webkit-text-fill-color':2634 'websearch':453,561,566 'websit':8,59,223 'width':605,808,1923 'win':2157 'window':879,976 'window.scrollto':991,1030,1074,1216,1312 'window.scrolly':1601 'window/body':915 'within':3698 'without':272,275,512 'won':2835 'wondering.app':888 'wordpress':766 'work':563,2461 'wrapper':895 'write':174 'www':273 'x':708 'xxx':3713 'y':900,955,985,987,989,993,996,1014,1016,1018,1021,1210,1212,1214,1218,1598,2782,3039 'z':1518,1605,1689,1718 'z-index':1517 'zero':100","prices":[{"id":"4c213849-60fa-4191-a5df-600a384e100a","listingId":"83d6838a-35c7-4e87-9ac8-c2d8863471f3","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"instantX-research","category":"skills","install_from":"skills.sh"},"createdAt":"2026-04-23T13:03:56.472Z"}],"sources":[{"listingId":"83d6838a-35c7-4e87-9ac8-c2d8863471f3","source":"github","sourceId":"instantX-research/skills/frontend-ui-clone","sourceUrl":"https://github.com/instantX-research/skills/tree/main/skills/frontend-ui-clone","isPrimary":false,"firstSeenAt":"2026-04-23T13:03:56.472Z","lastSeenAt":"2026-04-24T01:03:23.949Z"}],"details":{"listingId":"83d6838a-35c7-4e87-9ac8-c2d8863471f3","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"instantX-research","slug":"frontend-ui-clone","github":{"repo":"instantX-research/skills","stars":11,"topics":["agent-skills","frontend-ui","ui-design","web-search"],"license":"mit","html_url":"https://github.com/instantX-research/skills","pushed_at":"2026-04-08T11:28:55Z","description":"Open source skills for Agent 🔥","skill_md_sha":"1e44990e66d74f92eec02297527a4f3bd61cea95","skill_md_path":"skills/frontend-ui-clone/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/instantX-research/skills/tree/main/skills/frontend-ui-clone"},"layout":"multi","source":"github","category":"skills","frontmatter":{"name":"frontend-ui-clone","description":"Pixel-perfect website cloner. Given a URL, faithfully reproduces the page as a single\nself-contained HTML file. Uses Playwright to render the page in a real browser, then\ndirectly extracts the rendered DOM + all computed CSS — no manual rewriting.\nHandles static HTML, JS-rendered SPAs, Webflow, Next.js, and any framework automatically.\nZero creative interpretation — reproduces exactly what exists.\nUse when asked to \"clone this site\", \"copy this page\", \"replicate this URL\",\n\"pixel-perfect clone\", or user provides a URL and says \"make it look exactly like this\"."},"skills_sh_url":"https://skills.sh/instantX-research/skills/frontend-ui-clone"},"updatedAt":"2026-04-24T01:03:23.949Z"}}