Files
pbakaus_impeccable/site/public/docs/clarify.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

537 lines
27 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 clarify | Impeccable</title>
<meta name="description" content="Rewrite confusing UX copy so interfaces explain themselves.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/docs/clarify">
<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">/clarify</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">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" aria-current="page">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> / Simplify</p>
<h1 class="skill-detail-title"><span class="skill-detail-title-namespace"><span class="skill-detail-title-slash">/</span>impeccable</span>clarify</h1>
<p class="skill-detail-tagline">Rewrite confusing UX copy so interfaces explain themselves.</p>
<div class="skill-meta-strip">
<span class="skill-meta-chip skill-meta-category" data-category="simplify">Simplify</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-clarify">
<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%; max-width: 260px; display: flex; flex-direction: column; gap: 12px;">
<div style="padding: 12px; background: #f5f5f5; border-radius: 6px;">
<div style="font-size: 13px; font-weight: 600; margin-bottom: 4px;">Processing Status</div>
<div style="font-size: 12px; color: #666;">Your request is being processed. Please wait while we complete the operation. This may take some time depending on various factors.</div>
</div>
<div style="padding: 12px; background: #fff8e1; border-radius: 6px;">
<div style="font-size: 13px; font-weight: 600; margin-bottom: 4px;">⚠️ Warning</div>
<div style="font-size: 12px; color: #666;">Proceeding with this action may result in irreversible consequences to your data and settings configuration.</div>
</div>
<button style="padding: 10px; background: #333; color: white; border: none; border-radius: 4px; font-size: 13px;">Submit Request</button>
</div>
</div>
</div>
<div class="split-after">
<div class="split-content">
<div style="width: 100%; max-width: 260px; display: flex; flex-direction: column; gap: 12px;">
<div style="padding: 12px; background: var(--color-paper); border: 1px solid var(--color-mist); border-radius: 6px;">
<div style="font-size: 0.8125rem; font-weight: 600; color: var(--color-ink); margin-bottom: 4px;">Saving changes...</div>
<div style="font-size: 0.75rem; color: var(--color-ash);">About 10 seconds remaining</div>
<div style="margin-top: 8px; height: 4px; background: var(--color-mist); border-radius: 2px; overflow: hidden;">
<div style="width: 60%; height: 100%; background: var(--color-accent);"></div>
</div>
</div>
<div style="padding: 12px; background: #fef3c7; border: 1px solid #fcd34d; border-radius: 6px;">
<div style="font-size: 0.8125rem; font-weight: 600; color: #92400e; margin-bottom: 4px;">Delete this project?</div>
<div style="font-size: 0.75rem; color: #854d0e; line-height: 1.5;">This will permanently delete 23 files. You can't undo this.</div>
</div>
<button style="padding: 10px; background: var(--color-ink); color: var(--color-paper); border: none; border-radius: 6px; font-size: 0.8125rem; font-weight: 500;">Save and Continue →</button>
</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">Confusing copy → Clear, actionable language</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 clarify</code> is for interface text that makes people stop and think. Confusing labels, ambiguous button copy, error messages that blame the user, tooltips that repeat the label, empty states that say nothing useful. Use it when the problem is not the layout or the color, it is the words.</p>
<p>Good triggers: &quot;users do not understand this field&quot;, &quot;the error message is not helpful&quot;, &quot;I cannot write good button copy&quot;, &quot;this tooltip is a waste&quot;.</p>
<h2 id="how-it-works">How it works</h2>
<p>The skill rewrites text across the surfaces where most UX copy problems live:</p>
<ol>
<li><strong>Labels and field hints</strong>: direct, specific, say what is expected.</li>
<li><strong>Button copy</strong>: verb-first, describes the outcome, not the action. &quot;Save changes&quot; not &quot;OK&quot;.</li>
<li><strong>Error messages</strong>: explain what went wrong, whose fault it is, and what to do next. Never blame the user.</li>
<li><strong>Empty states</strong>: orient the user, explain why the state is empty, offer a next step.</li>
<li><strong>Tooltips and helper text</strong>: add information the label cannot carry, never restate it.</li>
<li><strong>Confirmation dialogs</strong>: name the consequences, not the action.</li>
</ol>
<p>The skill uses the audience and mental state from <code>PRODUCT.md</code> to tune voice. Technical audience gets precise language. Consumer audience gets plain speech. Rushed users get short text. Anxious users (payment, delete) get reassurance.</p>
<h2 id="try-it">Try it</h2>
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable clarify the billing form</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable clarify the billing form" aria-label="Copy to clipboard"></button></div>
<p>Before and after, typical:</p>
<ul>
<li>Label &quot;Billing address&quot;&quot;Address on your card&quot;</li>
<li>Placeholder &quot;Enter your VAT ID&quot;&quot;VAT ID (optional, for business)&quot;</li>
<li>Error &quot;Invalid input&quot;&quot;This card number is 15 digits. You entered 14.&quot;</li>
<li>Button &quot;Submit&quot;&quot;Charge $29 and subscribe&quot;</li>
<li>Empty state &quot;No transactions yet&quot;&quot;Your first charge will show up here after your first order.&quot;</li>
</ul>
<h2 id="pitfalls">Pitfalls</h2>
<ul>
<li><strong>Writing cleverer, not clearer.</strong> Clarify is not for voice upgrades. If the copy is already clear, do not reach for this skill. Use <code>/impeccable delight</code> instead when you want personality.</li>
<li><strong>Skipping the audience question.</strong> Clarify needs to know who is reading. If <code>PRODUCT.md</code> does not specify audience technical level, the rewrites will be generic.</li>
<li><strong>Running clarify on marketing copy.</strong> Clarify is for functional UX text: labels, errors, instructions. Marketing copy needs a different set of moves and a human writer.</li>
</ul>
</section>
<section class="skill-source-card">
<header class="skill-source-card-header">
<span class="skill-source-card-label">reference/clarify.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">
<blockquote>
<p><strong>Additional context needed</strong>: audience technical level and users&#39; mental state in context.</p>
</blockquote>
<p>Identify and improve unclear, confusing, or poorly written interface text to make the product easier to understand and use.</p>
<hr>
<h2 id="assess-current-copy">Assess Current Copy</h2>
<p>Identify what makes the text unclear or ineffective:</p>
<ol>
<li><p><strong>Find clarity problems</strong>:</p>
<ul>
<li><strong>Jargon</strong>: Technical terms users won&#39;t understand</li>
<li><strong>Ambiguity</strong>: Multiple interpretations possible</li>
<li><strong>Passive voice</strong>: &quot;Your file has been uploaded&quot; vs &quot;We uploaded your file&quot;</li>
<li><strong>Length</strong>: Too wordy or too terse</li>
<li><strong>Assumptions</strong>: Assuming user knowledge they don&#39;t have</li>
<li><strong>Missing context</strong>: Users don&#39;t know what to do or why</li>
<li><strong>Tone mismatch</strong>: Too formal, too casual, or inappropriate for situation</li>
</ul>
</li>
<li><p><strong>Understand the context</strong>:</p>
<ul>
<li>Who&#39;s the audience? (Technical? General? First-time users?)</li>
<li>What&#39;s the user&#39;s mental state? (Stressed during error? Confident during success?)</li>
<li>What&#39;s the action? (What do we want users to do?)</li>
<li>What&#39;s the constraint? (Character limits? Space limitations?)</li>
</ul>
</li>
</ol>
<p><strong>CRITICAL</strong>: Clear copy helps users succeed. Unclear copy creates frustration, errors, and support tickets.</p>
<h2 id="plan-copy-improvements">Plan Copy Improvements</h2>
<p>Create a strategy for clearer communication:</p>
<ul>
<li><strong>Primary message</strong>: What&#39;s the ONE thing users need to know?</li>
<li><strong>Action needed</strong>: What should users do next (if anything)?</li>
<li><strong>Tone</strong>: How should this feel? (Helpful? Apologetic? Encouraging?)</li>
<li><strong>Constraints</strong>: Length limits, brand voice, localization considerations</li>
</ul>
<p><strong>IMPORTANT</strong>: Good UX writing is invisible. Users should understand immediately without noticing the words.</p>
<h2 id="improve-copy-systematically">Improve Copy Systematically</h2>
<p>Refine text across these common areas:</p>
<h3 id="error-messages">Error Messages</h3>
<p><strong>Bad</strong>: &quot;Error 403: Forbidden&quot;
<strong>Good</strong>: &quot;You don&#39;t have permission to view this page. Contact your admin for access.&quot;</p>
<p><strong>Bad</strong>: &quot;Invalid input&quot;
<strong>Good</strong>: &quot;Email addresses need an @ symbol. Try: <a href="mailto:name@example.com" target="_blank" rel="noopener">name@example.com</a>&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Explain what went wrong in plain language</li>
<li>Suggest how to fix it</li>
<li>Don&#39;t blame the user</li>
<li>Include examples when helpful</li>
<li>Link to help/support if applicable</li>
</ul>
<h3 id="form-labels-instructions">Form Labels &amp; Instructions</h3>
<p><strong>Bad</strong>: &quot;DOB (MM/DD/YYYY)&quot;
<strong>Good</strong>: &quot;Date of birth&quot; (with placeholder showing format)</p>
<p><strong>Bad</strong>: &quot;Enter value here&quot;
<strong>Good</strong>: &quot;Your email address&quot; or &quot;Company name&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Use clear, specific labels (not generic placeholders)</li>
<li>Show format expectations with examples</li>
<li>Explain why you&#39;re asking (when not obvious)</li>
<li>Put instructions before the field, not after</li>
<li>Keep required field indicators clear</li>
</ul>
<h3 id="button-cta-text">Button &amp; CTA Text</h3>
<p><strong>Bad</strong>: &quot;Click here&quot; | &quot;Submit&quot; | &quot;OK&quot;
<strong>Good</strong>: &quot;Create account&quot; | &quot;Save changes&quot; | &quot;Got it, thanks&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Describe the action specifically</li>
<li>Use active voice (verb + noun)</li>
<li>Match user&#39;s mental model</li>
<li>Be specific (&quot;Save&quot; is better than &quot;OK&quot;)</li>
</ul>
<h3 id="help-text-tooltips">Help Text &amp; Tooltips</h3>
<p><strong>Bad</strong>: &quot;This is the username field&quot;
<strong>Good</strong>: &quot;Choose a username. You can change this later in Settings.&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Add value (don&#39;t just repeat the label)</li>
<li>Answer the implicit question (&quot;What is this?&quot; or &quot;Why do you need this?&quot;)</li>
<li>Keep it brief but complete</li>
<li>Link to detailed docs if needed</li>
</ul>
<h3 id="empty-states">Empty States</h3>
<p><strong>Bad</strong>: &quot;No items&quot;
<strong>Good</strong>: &quot;No projects yet. Create your first project to get started.&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Explain why it&#39;s empty (if not obvious)</li>
<li>Show next action clearly</li>
<li>Make it welcoming, not dead-end</li>
</ul>
<h3 id="success-messages">Success Messages</h3>
<p><strong>Bad</strong>: &quot;Success&quot;
<strong>Good</strong>: &quot;Settings saved! Your changes will take effect immediately.&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Confirm what happened</li>
<li>Explain what happens next (if relevant)</li>
<li>Be brief but complete</li>
<li>Match the user&#39;s emotional moment (celebrate big wins)</li>
</ul>
<h3 id="loading-states">Loading States</h3>
<p><strong>Bad</strong>: &quot;Loading...&quot; (for 30+ seconds)
<strong>Good</strong>: &quot;Analyzing your data... this usually takes 30-60 seconds&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Set expectations (how long?)</li>
<li>Explain what&#39;s happening (when it&#39;s not obvious)</li>
<li>Show progress when possible</li>
<li>Offer escape hatch if appropriate (&quot;Cancel&quot;)</li>
</ul>
<h3 id="confirmation-dialogs">Confirmation Dialogs</h3>
<p><strong>Bad</strong>: &quot;Are you sure?&quot;
<strong>Good</strong>: &quot;Delete &#39;Project Alpha&#39;? This can&#39;t be undone.&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>State the specific action</li>
<li>Explain consequences (especially for destructive actions)</li>
<li>Use clear button labels (&quot;Delete project&quot; not &quot;Yes&quot;)</li>
<li>Don&#39;t overuse confirmations (only for risky actions)</li>
</ul>
<h3 id="navigation-wayfinding">Navigation &amp; Wayfinding</h3>
<p><strong>Bad</strong>: Generic labels like &quot;Items&quot; | &quot;Things&quot; | &quot;Stuff&quot;
<strong>Good</strong>: Specific labels like &quot;Your projects&quot; | &quot;Team members&quot; | &quot;Settings&quot;</p>
<p><strong>Principles</strong>:</p>
<ul>
<li>Be specific and descriptive</li>
<li>Use language users understand (not internal jargon)</li>
<li>Make hierarchy clear</li>
<li>Consider information scent (breadcrumbs, current location)</li>
</ul>
<h2 id="apply-clarity-principles">Apply Clarity Principles</h2>
<p>Every piece of copy should follow these rules:</p>
<ol>
<li><strong>Be specific</strong>: &quot;Enter email&quot; not &quot;Enter value&quot;</li>
<li><strong>Be concise</strong>: Cut unnecessary words (but don&#39;t sacrifice clarity)</li>
<li><strong>Be active</strong>: &quot;Save changes&quot; not &quot;Changes will be saved&quot;</li>
<li><strong>Be human</strong>: &quot;Oops, something went wrong&quot; not &quot;System error encountered&quot;</li>
<li><strong>Be helpful</strong>: Tell users what to do, not just what happened</li>
<li><strong>Be consistent</strong>: Use same terms throughout (don&#39;t vary for variety)</li>
</ol>
<p><strong>NEVER</strong>:</p>
<ul>
<li>Use jargon without explanation</li>
<li>Blame users (&quot;You made an error&quot;&quot;This field is required&quot;)</li>
<li>Be vague (&quot;Something went wrong&quot; without explanation)</li>
<li>Use passive voice unnecessarily</li>
<li>Write overly long explanations (be concise)</li>
<li>Use humor for errors (be empathetic instead)</li>
<li>Assume technical knowledge</li>
<li>Vary terminology (pick one term and stick with it)</li>
<li>Repeat information (headers restating intros, redundant explanations)</li>
<li>Use placeholders as the only labels (they disappear when users type)</li>
</ul>
<h2 id="verify-improvements">Verify Improvements</h2>
<p>Test that copy improvements work:</p>
<ul>
<li><strong>Comprehension</strong>: Can users understand without context?</li>
<li><strong>Actionability</strong>: Do users know what to do next?</li>
<li><strong>Brevity</strong>: Is it as short as possible while remaining clear?</li>
<li><strong>Consistency</strong>: Does it match terminology elsewhere?</li>
<li><strong>Tone</strong>: Is it appropriate for the situation?</li>
</ul>
<p>Remember: You&#39;re a clarity expert with excellent communication skills. Write like you&#39;re explaining to a smart friend who&#39;s unfamiliar with the product. Be clear, be helpful, be human.</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>