Files
pbakaus_impeccable/site/public/docs/overdrive.html
T
Paul BakausandClaude Fable 5 90f9eeb99b Split service layer into private impeccable-site repo
The public repo keeps the OSS promise surface: skill, CLI, extension,
tests, and the provider build. The site, labs, concept/composition
catalogs, image pipeline, Cloudflare functions, and authoring guide move
to pbakaus/impeccable-site.

concept-seed tests run against a synthetic fixture catalog; the plugin
icon and skill categories moved in-repo; build validation narrows to
README prose and non-site counts; release notes read from a sibling
impeccable-site checkout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 22:41:53 -07:00

452 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>/impeccable overdrive | Impeccable</title>
<meta name="description" content="Push an interface past conventional limits. Shaders, physics, 60fps, cinematic transitions.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/docs/overdrive">
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/sub-pages.css">
</head>
<body class="sub-page skills-layout-page">
<a href="#main" class="skip-link">Skip to content</a>
<!-- site-header v1 -->
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/designing" data-nav="designing">Designing</a>
<a href="/docs" data-nav="docs" aria-current="page">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
<a href="/live-mode" data-nav="live">Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">21k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<main id="main">
<div class="skills-layout">
<aside class="skills-sidebar" aria-label="Documentation">
<button class="skills-sidebar-toggle" type="button" aria-expanded="false" aria-controls="skills-sidebar-inner">
<span class="skills-sidebar-toggle-label">/overdrive</span>
<svg class="skills-sidebar-toggle-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="skills-sidebar-inner" id="skills-sidebar-inner">
<p class="skills-sidebar-label">Docs</p>
<div class="skills-sidebar-group" data-category="tutorials">
<p class="skills-sidebar-group-title">Tutorials</p>
<ul class="skills-sidebar-list">
<li><a href="/tutorials/getting-started">Getting started</a></li>
<li><a href="/tutorials/iterate-live">Iterate on UI with Live Mode</a></li>
<li><a href="/tutorials/brand-vs-product">Brand vs product, pick a register</a></li>
<li><a href="/tutorials/critique-with-overlay">Critique with the visual overlay</a></li>
</ul>
</div>
<hr class="skills-sidebar-divider">
<div class="skills-sidebar-group" data-category="create">
<p class="skills-sidebar-group-title">Create</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/impeccable">/impeccable</a></li>
<li><a href="/docs/craft">craft</a></li>
<li><a href="/docs/shape">shape</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="evaluate">
<p class="skills-sidebar-group-title">Evaluate</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/audit">audit</a></li>
<li><a href="/docs/critique">critique</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="refine">
<p class="skills-sidebar-group-title">Refine</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/animate">animate</a></li>
<li><a href="/docs/bolder">bolder</a></li>
<li><a href="/docs/colorize">colorize</a></li>
<li><a href="/docs/delight">delight</a></li>
<li><a href="/docs/layout">layout</a></li>
<li><a href="/docs/overdrive" aria-current="page">overdrive</a></li>
<li><a href="/docs/quieter">quieter</a></li>
<li><a href="/docs/typeset">typeset</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="simplify">
<p class="skills-sidebar-group-title">Simplify</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/adapt">adapt</a></li>
<li><a href="/docs/clarify">clarify</a></li>
<li><a href="/docs/distill">distill</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="harden">
<p class="skills-sidebar-group-title">Harden</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/harden">harden</a></li>
<li><a href="/docs/onboard">onboard</a></li>
<li><a href="/docs/optimize">optimize</a></li>
<li><a href="/docs/polish">polish</a></li>
</ul>
</div>
<div class="skills-sidebar-group" data-category="system">
<p class="skills-sidebar-group-title">System</p>
<ul class="skills-sidebar-list">
<li><a href="/docs/document">document</a></li>
<li><a href="/docs/extract">extract</a></li>
<li><a href="/docs/live">live</a></li>
<li><a href="/docs/teach">teach</a></li>
</ul>
</div>
</div>
</aside>
<div class="skills-main">
<article class="skill-detail">
<div class="skill-detail-hero skill-detail-hero--has-demo">
<header class="skill-detail-header">
<p class="skill-detail-eyebrow"><a href="/docs">Docs</a> / Refine</p>
<h1 class="skill-detail-title"><span class="skill-detail-title-namespace"><span class="skill-detail-title-slash">/</span>impeccable</span>overdrive</h1>
<p class="skill-detail-tagline">Push an interface past conventional limits. Shaders, physics, 60fps, cinematic transitions.</p>
<div class="skill-meta-strip">
<span class="skill-meta-chip skill-meta-category" data-category="refine">Refine</span>
<span class="skill-meta-chip">User-invocable</span>
<span class="skill-meta-chip skill-meta-args">[target]</span>
</div>
</header>
<section class="skill-demo" aria-label="Before and after demo">
<div class="split-comparison" data-demo="skill-overdrive">
<p class="skill-demo-eyebrow">Drag or hover to compare</p>
<div class="split-container">
<div class="split-before">
<div class="split-content">
<div style="width: 100%; height: 100%; min-height: 200px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; font-family: system-ui, sans-serif;">
<div style="text-align: center; padding: 20px;">
<div style="font-size: 12px; color: #666; font-style: italic; line-height: 1.6; max-width: 220px; margin-bottom: 16px;">It's time to spark your imagination. Welcome to the Impeccable Community.</div>
<div style="font-size: 12px; color: #aaa;">Paul Bakaus</div>
</div>
</div>
</div>
</div>
<div class="split-after">
<div class="split-content">
<canvas class="od-burn" style="position: absolute; inset: 0; width: 100%; height: 100%; background: #0e0d0b;"></canvas>
<canvas class="od-sparks" style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;"></canvas>
<div style="position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; pointer-events: none; text-align: center;">
<p style="font-family: 'Cormorant Garamond', serif; font-size: 1.1rem; font-style: italic; font-weight: 400; color: rgba(240,230,210,0.85); line-height: 1.5; max-width: 260px; margin: 0 0 24px;">It's time to spark your imagination.<br>Welcome to the Impeccable Community.</p>
</div>
</div>
</div>
<div class="split-divider"></div>
</div>
<div class="split-labels">
<span class="split-label-item" data-point="before">Before</span>
<p class="skill-demo-caption">Static flat card → Laser-etched signature effect</p>
<span class="split-label-item" data-point="after">After</span>
</div>
</div>
</section>
</div>
<section class="skill-detail-editorial prose">
<h2 id="when-to-use-it">When to use it</h2>
<p><code>/impeccable overdrive</code> is for the moments where you want to impress. A hero that uses WebGL. A table that handles a million rows. A dialog that morphs out of its trigger element. A form that validates in real-time with streaming feedback. A page transition that feels cinematic. Use it when the project budget allows for technical ambition and the outcome needs to feel extraordinary.</p>
<p>Do not use it on operator tools, dashboards, or anything where reliability beats spectacle. Overdrive burns complexity for effect, and that trade-off is only worth it on moments that matter.</p>
<h2 id="how-it-works">How it works</h2>
<p>The skill picks one moment to make extraordinary and commits to it, rather than spreading effort across the whole interface. It then reaches for techniques most AI-generated UIs never touch: WebGL shaders, spring physics, Scroll Timeline, View Transitions, canvas animation, GPU-accelerated filters. Everything is budgeted, profiled, and tested at 60fps, with reduced-motion fallbacks baked in.</p>
<p>Overdrive output is announced with <code>──── ⚡ OVERDRIVE ────</code> so you know you are entering a more ambitious mode. Expect larger diffs, new dependencies, and implementation depth beyond what other skills produce.</p>
<h2 id="try-it">Try it</h2>
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable overdrive the landing hero</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable overdrive the landing hero" aria-label="Copy to clipboard"></button></div>
<p>One concrete run might replace a static hero with a WebGL shader background driven by mouse position, a display headline that reveals with a mask on scroll using the Scroll Timeline API, and a View Transition on the CTA that morphs into the next page. Plus a reduced-motion fallback that swaps all of it for a clean static composition.</p>
<h2 id="pitfalls">Pitfalls</h2>
<ul>
<li><strong>Using it everywhere.</strong> Overdrive works because it is rare. If every page has cinematic moments, none of them are cinematic.</li>
<li><strong>Shipping without reduced-motion fallbacks.</strong> Non-negotiable. Overdrive adds them automatically; do not remove them.</li>
<li><strong>Ignoring performance.</strong> Extraordinary moments still need to hit 60fps. If the effect drops frames, cut it or optimize. Slow spectacle is worse than simple done well.</li>
<li><strong>Running overdrive before the base interface is solid.</strong> Spectacle on a broken foundation reads as distraction, not delight.</li>
</ul>
</section>
<section class="skill-source-card">
<header class="skill-source-card-header">
<span class="skill-source-card-label">reference/overdrive.md</span>
<span class="skill-source-card-subtitle">Loaded when the impeccable skill routes to this command.</span>
</header>
<div class="skill-source-card-body prose">
<p>Start your response with:</p>
<div class="code-block-wrap"><pre class="code-block"><code>──────────── ⚡ OVERDRIVE ─────────────
》》》 Entering overdrive mode...</code></pre><button class="code-block-copy" type="button" data-copy="──────────── ⚡ OVERDRIVE ─────────────
》》》 Entering overdrive mode..." aria-label="Copy to clipboard"></button></div>
<p>Push an interface past conventional limits. This isn&#39;t just about visual effects. It&#39;s about using the full power of the browser to make any part of an interface feel extraordinary: a table that handles a million rows, a dialog that morphs from its trigger, a form that validates in real-time with streaming feedback, a page transition that feels cinematic.</p>
<p><strong>EXTRA IMPORTANT FOR THIS COMMAND</strong>: Context determines what &quot;extraordinary&quot; means. A particle system on a creative portfolio is impressive. The same particle system on a settings page is embarrassing. But a settings page with instant optimistic saves and animated state transitions? That&#39;s extraordinary too. Understand the project&#39;s personality and goals before deciding what&#39;s appropriate.</p>
<h3 id="propose-before-building">Propose Before Building</h3>
<p>This command has the highest potential to misfire. Do NOT jump straight into implementation. You MUST:</p>
<ol>
<li><strong>Think through 2-3 different directions</strong>: consider different techniques, levels of ambition, and aesthetic approaches. For each direction, briefly describe what the result would look and feel like.</li>
<li><strong>STOP and call the AskUserQuestion tool to clarify.</strong> to present these directions and get the user&#39;s pick before writing any code. Explain trade-offs (browser support, performance cost, complexity).</li>
<li>Only proceed with the direction the user confirms.</li>
</ol>
<p>Skipping this step risks building something embarrassing that needs to be thrown away.</p>
<h3 id="iterate-with-browser-automation">Iterate with Browser Automation</h3>
<p>Technically ambitious effects almost never work on the first try. You MUST actively use browser automation tools to preview your work, visually verify the result, and iterate. Do not assume the effect looks right, check it. Expect multiple rounds of refinement. The gap between &quot;technically works&quot; and &quot;looks extraordinary&quot; is closed through visual iteration, not code alone.</p>
<hr>
<h2 id="assess-what-extraordinary-means-here">Assess What &quot;Extraordinary&quot; Means Here</h2>
<p>The right kind of technical ambition depends entirely on what you&#39;re working with. Before choosing a technique, ask: <strong>what would make a user of THIS specific interface say &quot;wow, that&#39;s nice&quot;?</strong></p>
<h3 id="for-visualmarketing-surfaces">For visual/marketing surfaces</h3>
<p>Pages, hero sections, landing pages, portfolios — the &quot;wow&quot; is often sensory: a scroll-driven reveal, a shader background, a cinematic page transition, generative art that responds to the cursor.</p>
<h3 id="for-functional-ui">For functional UI</h3>
<p>Tables, forms, dialogs, navigation — the &quot;wow&quot; is in how it FEELS: a dialog that morphs from the button that triggered it via View Transitions, a data table that renders 100k rows at 60fps via virtual scrolling, a form with streaming validation that feels instant, drag-and-drop with spring physics.</p>
<h3 id="for-performance-critical-ui">For performance-critical UI</h3>
<p>The &quot;wow&quot; is invisible but felt: a search that filters 50k items without a flicker, a complex form that never blocks the main thread, an image editor that processes in near-real-time. The interface just never hesitates.</p>
<h3 id="for-data-heavy-interfaces">For data-heavy interfaces</h3>
<p>Charts and dashboards — the &quot;wow&quot; is in fluidity: GPU-accelerated rendering via Canvas/WebGL for massive datasets, animated transitions between data states, force-directed graph layouts that settle naturally.</p>
<p><strong>The common thread</strong>: something about the implementation goes beyond what users expect from a web interface. The technique serves the experience, not the other way around.</p>
<h2 id="the-toolkit">The Toolkit</h2>
<p>Organized by what you&#39;re trying to achieve, not by technology name.</p>
<h3 id="make-transitions-feel-cinematic">Make transitions feel cinematic</h3>
<ul>
<li><strong>View Transitions API</strong> (same-document: all browsers; cross-document: no Firefox) — shared element morphing between states. A list item expanding into a detail page. A button morphing into a dialog. This is the closest thing to native FLIP animations.</li>
<li><strong><code>@starting-style</code></strong> (all browsers) — animate elements from <code>display: none</code> to visible with CSS only, including entry keyframes</li>
<li><strong>Spring physics</strong> — natural motion with mass, tension, and damping instead of cubic-bezier. Libraries: motion (formerly Framer Motion), GSAP, or roll your own spring solver.</li>
</ul>
<h3 id="tie-animation-to-scroll-position">Tie animation to scroll position</h3>
<ul>
<li><strong>Scroll-driven animations</strong> (<code>animation-timeline: scroll()</code>) — CSS-only, no JS. Parallax, progress bars, reveal sequences all driven by scroll position. (Chrome/Edge/Safari; Firefox: flag only — always provide a static fallback)</li>
</ul>
<h3 id="render-beyond-css">Render beyond CSS</h3>
<ul>
<li><strong>WebGL</strong> (all browsers) — shader effects, post-processing, particle systems. Libraries: Three.js, OGL (lightweight), regl. Use for effects CSS can&#39;t express.</li>
<li><strong>WebGPU</strong> (Chrome/Edge; Safari partial; Firefox: flag only) — next-gen GPU compute. More powerful than WebGL but limited browser support. Always fall back to WebGL2.</li>
<li><strong>Canvas 2D / OffscreenCanvas</strong> — custom rendering, pixel manipulation, or moving heavy rendering off the main thread entirely via Web Workers + OffscreenCanvas.</li>
<li><strong>SVG filter chains</strong> — displacement maps, turbulence, morphology for organic distortion effects. CSS-animatable.</li>
</ul>
<h3 id="make-data-feel-alive">Make data feel alive</h3>
<ul>
<li><strong>Virtual scrolling</strong> — render only visible rows for tables/lists with tens of thousands of items. No library required for simple cases; TanStack Virtual for complex ones.</li>
<li><strong>GPU-accelerated charts</strong> — Canvas or WebGL-rendered data visualization for datasets too large for SVG/DOM. Libraries: deck.gl, regl-based custom renderers.</li>
<li><strong>Animated data transitions</strong> — morph between chart states rather than replacing. D3&#39;s <code>transition()</code> or View Transitions for DOM-based charts.</li>
</ul>
<h3 id="animate-complex-properties">Animate complex properties</h3>
<ul>
<li><strong><code>@property</code></strong> (all browsers) — register custom CSS properties with types, enabling animation of gradients, colors, and complex values that CSS can&#39;t normally interpolate.</li>
<li><strong>Web Animations API</strong> (all browsers) — JavaScript-driven animations with the performance of CSS. Composable, cancellable, reversible. The foundation for complex choreography.</li>
</ul>
<h3 id="push-performance-boundaries">Push performance boundaries</h3>
<ul>
<li><strong>Web Workers</strong> — move computation off the main thread. Heavy data processing, image manipulation, search indexing — anything that would cause jank.</li>
<li><strong>OffscreenCanvas</strong> — render in a Worker thread. The main thread stays free while complex visuals render in the background.</li>
<li><strong>WASM</strong> — near-native performance for computation-heavy features. Image processing, physics simulations, codecs.</li>
</ul>
<h3 id="interact-with-the-device">Interact with the device</h3>
<ul>
<li><strong>Web Audio API</strong> — spatial audio, audio-reactive visualizations, sonic feedback. Requires user gesture to start.</li>
<li><strong>Device APIs</strong> — orientation, ambient light, geolocation. Use sparingly and always with user permission.</li>
</ul>
<p><strong>NOTE</strong>: This command is about enhancing how an interface FEELS, not changing what a product DOES. Adding real-time collaboration, offline support, or new backend capabilities are product decisions, not UI enhancements. Focus on making existing features feel extraordinary.</p>
<h2 id="implement-with-discipline">Implement with Discipline</h2>
<h3 id="progressive-enhancement-is-non-negotiable">Progressive enhancement is non-negotiable</h3>
<p>Every technique must degrade gracefully. The experience without the enhancement must still be good.</p>
<div class="code-block-wrap"><pre class="code-block code-block--css"><code>@supports (animation-timeline: scroll()) {
.hero { animation-timeline: scroll(); }
}</code></pre><button class="code-block-copy" type="button" data-copy="@supports (animation-timeline: scroll()) {
.hero { animation-timeline: scroll(); }
}" aria-label="Copy to clipboard"></button></div>
<div class="code-block-wrap"><pre class="code-block code-block--javascript"><code>if (&#39;gpu&#39; in navigator) { /* WebGPU */ }
else if (canvas.getContext(&#39;webgl2&#39;)) { /* WebGL2 fallback */ }
/* CSS-only fallback must still look good */</code></pre><button class="code-block-copy" type="button" data-copy="if ('gpu' in navigator) { /* WebGPU */ }
else if (canvas.getContext('webgl2')) { /* WebGL2 fallback */ }
/* CSS-only fallback must still look good */" aria-label="Copy to clipboard"></button></div>
<h3 id="performance-rules">Performance rules</h3>
<ul>
<li>Target 60fps. If dropping below 50, simplify.</li>
<li>Respect <code>prefers-reduced-motion</code> — always. Provide a beautiful static alternative.</li>
<li>Lazy-initialize heavy resources (WebGL contexts, WASM modules) only when near viewport.</li>
<li>Pause off-screen rendering. Kill what you can&#39;t see.</li>
<li>Test on real mid-range devices, not just your development machine.</li>
</ul>
<h3 id="polish-is-the-difference">Polish is the difference</h3>
<p>The gap between &quot;cool&quot; and &quot;extraordinary&quot; is in the last 20% of refinement: the easing curve on a spring animation, the timing offset in a staggered reveal, the subtle secondary motion that makes a transition feel physical. Don&#39;t ship the first version that works — ship the version that feels inevitable.</p>
<p><strong>NEVER</strong>:</p>
<ul>
<li>Ignore <code>prefers-reduced-motion</code> — this is an accessibility requirement, not a suggestion</li>
<li>Ship effects that cause jank on mid-range devices</li>
<li>Use bleeding-edge APIs without a functional fallback</li>
<li>Add sound without explicit user opt-in</li>
<li>Use technical ambition to mask weak design fundamentals; fix those first with other commands</li>
<li>Layer multiple competing extraordinary moments — focus creates impact, excess creates noise</li>
</ul>
<h2 id="verify-the-result">Verify the Result</h2>
<ul>
<li><strong>The wow test</strong>: Show it to someone who hasn&#39;t seen it. Do they react?</li>
<li><strong>The removal test</strong>: Take it away. Does the experience feel diminished, or does nobody notice?</li>
<li><strong>The device test</strong>: Run it on a phone, a tablet, a Chromebook. Still smooth?</li>
<li><strong>The accessibility test</strong>: Enable reduced motion. Still beautiful?</li>
<li><strong>The context test</strong>: Does this make sense for THIS brand and audience?</li>
</ul>
<p>Remember: &quot;Technically extraordinary&quot; isn&#39;t about using the newest API. It&#39;s about making an interface do something users didn&#39;t think a website could do.</p>
</div>
</section>
</article>
</div>
</div>
</main>
<script>
// Copy buttons on rendered code blocks
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// Pointer events attach to the padded .split-comparison wrapper so
// there is a ~20px invisible buffer around the visible box. The
// divider only snaps back when the pointer leaves that outer buffer.
(function initSplitCompare() {
const wrappers = document.querySelectorAll('.split-comparison');
if (wrappers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
for (const wrapper of wrappers) {
const container = wrapper.querySelector('.split-container');
const splitAfter = wrapper.querySelector('.split-after');
const splitDivider = wrapper.querySelector('.split-divider');
if (!container || !splitAfter || !splitDivider) continue;
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const paint = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath =
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
splitDivider.style.left = `${x}%`;
};
const step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
paint(DEFAULT_POSITION);
// Percentage is always relative to the VISIBLE .split-container,
// not the padded .split-comparison wrapper. The pointer event
// target is the wrapper but the clip-path math uses the inner box.
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
};
let hovering = false;
let dragging = false;
wrapper.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') {
hovering = true;
}
});
wrapper.addEventListener('pointerdown', (e) => {
dragging = true;
wrapper.setPointerCapture(e.pointerId);
setTarget(pctFromClientX(e.clientX));
});
wrapper.addEventListener('pointermove', (e) => {
if (dragging || hovering) {
setTarget(pctFromClientX(e.clientX));
}
});
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
}
};
wrapper.addEventListener('pointerup', endDrag);
wrapper.addEventListener('pointercancel', endDrag);
wrapper.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>
</body>
</html>