refine(site): "The Case" crossfade + content cleanup, pattern-tabs scroll

- why-panel tab swap is a proper opacity+transform crossfade (display:grid
  stack area) instead of display:none jump; 650/800ms ease-out.
- tab progress indicator animates linearly (timer, not eased).
- Panel 01: dropped redundant "Every command reads this…" footer; moved the
  commands meta into the visual as a right-side sidebar aside PRODUCT.md.
- Panel 02: "Browse the full catalog →" moved under the Gallery of Shame;
  pattern category tabs are now always a single-row horizontal scroll with
  JS-tracked edge-fade mask and chevron affordances; click centers the
  selected tab inside the strip (never scrolls the page).
- Panel 04: "register" → "mode" in body, labels, meta for plain-language.
- Panel 05: removed redundant "Works in Claude Code…" meta.
- Panel 06: removed "Spec-compliant. Interoperable. Not a proprietary
  sidecar." meta.
- .language-content grid gap reduced from --spacing-lg to --spacing-sm so
  the commands palette sits closer to the section lead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-22 18:47:33 -07:00
co-authored by Claude Opus 4.7
parent 2341fe3637
commit 3ffc485a8d
10 changed files with 2041 additions and 852 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
<a href="/" data-nav="home">Home</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
<a href="/docs/live" data-nav="live">Live</a>
<a href="/live-mode" data-nav="live">Live</a>
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
+5
View File
@@ -2,6 +2,11 @@
tagline: "Iterate on UI in the browser. Pick an element, drop a comment, get three variants. Accept one and it writes to source."
---
<div class="docs-live-callout">
<span class="docs-live-callout-icon" aria-hidden="true">▸</span>
<span class="docs-live-callout-text">See it in action, with the animated demo, at <a href="/live-mode">/live-mode</a>. This page is the reference for what your AI harness reads when the command runs.</span>
</div>
<div class="docs-viz-hero docs-viz-hero--plain">
<div class="docs-viz-live-frame">
<div class="docs-viz-live-chrome">
+31 -1
View File
@@ -153,7 +153,10 @@ function renderPatternsWithTabs(patterns, antipatterns) {
</div>`;
}).join('');
container.innerHTML = `<div class="patterns-tabs">${tabsHTML}</div>${panelsHTML}`;
container.innerHTML = `<div class="patterns-tabs-wrap"><div class="patterns-tabs" data-scroll="start">${tabsHTML}</div></div>${panelsHTML}`;
const tabsEl = container.querySelector('.patterns-tabs');
const tabsWrap = container.querySelector('.patterns-tabs-wrap');
container.addEventListener('click', (e) => {
const tab = e.target.closest('.patterns-tab');
@@ -163,7 +166,34 @@ function renderPatternsWithTabs(patterns, antipatterns) {
container.querySelectorAll('.patterns-content').forEach(p => p.classList.remove('is-active'));
tab.classList.add('is-active');
container.querySelector(`.patterns-content[data-index="${index}"]`).classList.add('is-active');
// Center the clicked tab inside the tabs strip (not the page). Using
// scrollBy on the container keeps the page scroll untouched.
if (tabsEl) {
const tabRect = tab.getBoundingClientRect();
const stripRect = tabsEl.getBoundingClientRect();
const offset = (tabRect.left + tabRect.width / 2) - (stripRect.left + stripRect.width / 2);
tabsEl.scrollBy({ left: offset, behavior: 'smooth' });
}
});
// Track scroll position so the edge-fade mask only appears on sides where
// there's actually more content. At the start, no left fade; at the end,
// no right fade; if no overflow, no fade at all.
const updateScrollState = () => {
if (!tabsEl) return;
const { scrollLeft, scrollWidth, clientWidth } = tabsEl;
const max = scrollWidth - clientWidth;
let state;
if (max <= 1) state = 'none';
else if (scrollLeft <= 1) state = 'start';
else if (scrollLeft >= max - 1) state = 'end';
else state = 'middle';
tabsEl.dataset.scroll = state;
if (tabsWrap) tabsWrap.dataset.scroll = state;
};
tabsEl?.addEventListener('scroll', updateScrollState, { passive: true });
window.addEventListener('resize', updateScrollState);
updateScrollState();
}
// ============================================
+368
View File
@@ -1255,3 +1255,371 @@
content: "✓";
font-weight: 700;
}
/* ============================================
LIVE MODE LANDING PAGE (/live-mode)
Marketing-style landing that mirrors /visual-mode's structure.
Hosts the animated homepage live-demo and surfaces the tutorial,
reference, and install pathways.
============================================ */
.live-mode-page {
display: flex;
flex-direction: column;
gap: clamp(2.5rem, 5vw, 3.5rem);
max-width: 960px;
margin: 0 auto;
padding: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 3vw, 2rem) clamp(4rem, 8vw, 6rem);
}
.live-mode-page-header {
display: flex;
flex-direction: column;
gap: 10px;
max-width: 56ch;
}
.live-mode-page-eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
font-family: var(--font-body);
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--color-accent);
}
.live-mode-page-eyebrow-badge {
padding: 2px 7px;
border: 1px solid var(--color-accent);
border-radius: 4px;
font-size: 0.625rem;
letter-spacing: 0.12em;
}
.live-mode-page-title {
font-family: var(--font-display);
font-weight: 400;
font-size: clamp(2.5rem, 5vw, 3.5rem);
line-height: 1;
letter-spacing: -0.02em;
color: var(--color-ink);
margin: 0;
}
.live-mode-page-lede {
font-family: var(--font-body);
font-size: 1.0625rem;
line-height: 1.55;
color: var(--color-charcoal);
margin: 0;
max-width: 56ch;
}
.live-mode-start {
display: flex;
align-items: center;
gap: 10px;
margin-top: 6px;
padding: 10px 14px;
background: var(--color-ink);
color: var(--color-paper);
border-radius: 8px;
font-family: var(--font-mono);
font-size: 0.875rem;
align-self: flex-start;
max-width: 100%;
}
.live-mode-start-prompt {
color: var(--color-accent);
user-select: none;
}
.live-mode-start-cmd {
flex: 1;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
}
.live-mode-start-cmd::-webkit-scrollbar { display: none; }
.live-mode-start-copy {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
color: oklch(75% 0 0);
border: 0;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
}
.live-mode-start-copy:hover {
background: oklch(22% 0 0);
color: oklch(92% 0 0);
}
.live-mode-start-copy.is-copied {
color: oklch(70% 0.18 145);
}
/* Demo section — wraps the animated homepage .live-demo block */
.live-mode-demo-wrap {
margin: 0;
}
.live-mode-demo-caption {
margin: 14px 0 0;
font-family: var(--font-body);
font-size: 0.875rem;
color: var(--color-ash);
line-height: 1.55;
font-style: italic;
}
/* Three-card "what happens" section */
.live-mode-stages {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.live-mode-stages-title {
font-family: var(--font-display);
font-weight: 400;
font-style: italic;
font-size: 1.75rem;
color: var(--color-ink);
margin: 0 0 8px 0;
}
.live-mode-stages-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-md);
}
@media (max-width: 720px) {
.live-mode-stages-grid {
grid-template-columns: 1fr;
}
}
.live-mode-stage {
display: flex;
flex-direction: column;
gap: 10px;
padding: 18px;
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 8px;
}
.live-mode-stage-num {
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.12em;
color: var(--color-ash);
text-transform: uppercase;
}
.live-mode-stage-name {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 1.375rem;
color: var(--color-ink);
margin: 0;
line-height: 1.15;
}
.live-mode-stage-desc {
font-family: var(--font-body);
font-size: 0.875rem;
line-height: 1.55;
color: var(--color-charcoal);
margin: 0;
}
.live-mode-stage-viz {
margin-top: 4px;
padding: 14px;
background: var(--color-cream);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
min-height: 92px;
}
/* Three pathway cards — tutorial, reference, install */
.live-mode-pathways {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.live-mode-pathways-title {
font-family: var(--font-display);
font-weight: 400;
font-style: italic;
font-size: 1.75rem;
color: var(--color-ink);
margin: 0 0 8px 0;
}
.live-mode-pathways-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-md);
}
@media (max-width: 720px) {
.live-mode-pathways-grid {
grid-template-columns: 1fr;
}
}
.live-mode-pathway {
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px;
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 8px;
text-decoration: none;
transition: border-color 180ms ease, transform 180ms ease;
}
.live-mode-pathway:hover {
border-color: var(--color-accent);
}
.live-mode-pathway-kind {
font-family: var(--font-mono);
font-size: 0.625rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--color-accent);
}
.live-mode-pathway-title {
font-family: var(--font-body);
font-size: 1.125rem;
font-weight: 600;
color: var(--color-ink);
margin: 0;
line-height: 1.3;
}
.live-mode-pathway-desc {
font-family: var(--font-body);
font-size: 0.875rem;
color: var(--color-charcoal);
line-height: 1.55;
margin: 0;
flex: 1;
}
.live-mode-pathway-cta {
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-accent);
margin-top: 6px;
}
/* Frameworks strip */
.live-mode-frameworks {
display: flex;
flex-direction: column;
gap: 10px;
padding: 18px 20px;
background: var(--color-cream);
border: 1px solid var(--color-mist);
border-radius: 8px;
}
.live-mode-frameworks-label {
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.12em;
color: var(--color-ash);
text-transform: uppercase;
}
.live-mode-frameworks-list {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-family: var(--font-body);
font-size: 0.875rem;
color: var(--color-ink);
margin: 0;
padding: 0;
list-style: none;
}
.live-mode-frameworks-list li {
display: inline-flex;
align-items: center;
gap: 6px;
}
.live-mode-frameworks-list li::before {
content: "";
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--color-accent);
}
/* Docs/live page callout pointing back to /live-mode */
.docs-live-callout {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 16px;
margin: 0 0 clamp(1.25rem, 2.5vw, 1.75rem);
background: var(--color-accent-dim);
border: 1px solid var(--color-accent-soft);
border-radius: 8px;
font-family: var(--font-body);
font-size: 0.875rem;
color: var(--color-ink);
line-height: 1.5;
}
.docs-live-callout-icon {
flex-shrink: 0;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--color-accent);
color: var(--color-paper);
border-radius: 50%;
font-size: 14px;
}
.docs-live-callout-text {
flex: 1;
}
.docs-live-callout-text a {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 2px;
font-weight: 600;
}
+802
View File
@@ -0,0 +1,802 @@
/* ============================================
LIVE MODE — interactive demo loop
============================================ */
.live-section {
padding: var(--spacing-2xl) 0;
border-top: 1px solid var(--color-mist);
}
.live-content .section-lead {
max-width: 64ch;
margin-bottom: var(--spacing-xl);
}
.live-demo {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
gap: var(--spacing-xl);
align-items: start;
margin-bottom: var(--spacing-xl);
}
.live-demo-frame-col {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
min-width: 0;
}
@media (max-width: 920px) {
.live-demo { grid-template-columns: 1fr; }
}
.live-demo-frame {
position: relative;
background: var(--color-cream);
border: 1px solid var(--color-mist);
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16 / 9;
max-width: 960px;
box-shadow: 0 20px 50px oklch(0% 0 0 / 0.08);
user-select: none;
}
.live-demo-chrome {
display: flex;
align-items: center;
gap: 7px;
padding: 10px 14px;
border-bottom: 1px solid var(--color-mist);
background: var(--color-paper);
}
.live-demo-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--color-mist);
}
.live-demo-url {
margin-left: 14px;
padding: 4px 14px;
background: var(--color-cream);
border: 1px solid var(--color-mist);
border-radius: 5px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-charcoal);
}
.live-demo-stage {
position: relative;
height: calc(100% - 42px - 60px); /* minus chrome + bar */
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
overflow: hidden;
}
/* Target card */
.live-demo-target {
position: relative;
width: min(360px, 80%);
min-height: 200px;
}
.live-demo-variant {
position: absolute;
inset: 0;
opacity: 0;
transform: translateY(8px) scale(0.99);
transition: opacity 280ms var(--ease-out), transform 280ms var(--ease-out);
pointer-events: none;
}
.live-demo-variant.is-active {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.live-demo-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 22px 24px;
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 8px;
height: 100%;
box-sizing: border-box;
}
.live-demo-card-kicker {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-ash);
}
.live-demo-card h3 {
font-family: var(--font-display);
font-weight: 400;
font-size: 20px;
line-height: 1.2;
color: var(--color-ink);
margin: 0;
}
.live-demo-card p {
font-family: var(--font-body);
font-size: 13px;
line-height: 1.5;
color: var(--color-charcoal);
margin: 0;
}
.live-demo-card button {
align-self: flex-start;
margin-top: auto;
font-family: var(--font-body);
font-weight: 500;
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 9px 16px;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 0;
cursor: pointer;
}
/* Per-variant look */
.live-demo-card--plain {}
.live-demo-card--v1 {
background: var(--color-cream);
}
.live-demo-card--v1 h3 { font-style: italic; }
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
.live-demo-card--v2 {
background: var(--color-paper);
border: 1px dashed var(--color-accent);
}
.live-demo-card--v2 .live-demo-card-kicker { color: var(--color-accent); }
.live-demo-card--v2 h3 { font-family: var(--font-display); font-style: italic; }
.live-demo-card--v2 button {
background: var(--color-accent);
}
.live-demo-card--v3 {
background: oklch(96% 0.02 350);
border: 1px solid var(--color-accent-soft);
}
.live-demo-card--v3 h3 em { color: var(--color-accent); font-style: italic; }
.live-demo-card--v3 .live-demo-card-kicker { color: var(--color-accent-hover); }
/* Highlight outline over the target */
.live-demo-outline {
position: absolute;
border: 2px solid var(--color-accent);
border-radius: 8px;
pointer-events: none;
opacity: 0;
transition: opacity 200ms var(--ease-out), top 320ms var(--ease-out), left 320ms var(--ease-out), width 320ms var(--ease-out), height 320ms var(--ease-out);
box-shadow: 0 0 0 4px var(--color-accent-dim);
}
.live-demo-outline.is-visible {
opacity: 1;
}
/* Annotations */
.live-demo-annotations {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0;
transition: opacity 280ms var(--ease-out);
color: var(--color-accent);
}
.live-demo-annotations.is-visible { opacity: 1; }
.live-demo-stroke {
position: absolute;
width: 280px;
height: 56px;
top: 50%;
left: 50%;
transform: translate(-50%, -24px);
pointer-events: none;
}
.live-demo-stroke path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.live-demo-annotations.is-visible .live-demo-stroke path {
animation: liveDemoStroke 800ms var(--ease-out) forwards;
}
@keyframes liveDemoStroke {
to { stroke-dashoffset: 0; }
}
.live-demo-comment {
position: absolute;
top: 56%;
left: 50%;
transform: translate(-50%, 0);
padding: 5px 10px;
background: var(--color-ink);
color: var(--color-paper);
font-family: var(--font-mono);
font-size: 11px;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
transition: opacity 200ms var(--ease-out);
}
.live-demo-annotations.is-comment-visible .live-demo-comment { opacity: 1; }
/* Simulated cursor */
.live-demo-cursor {
position: absolute;
top: 0;
left: 0;
width: 18px;
height: 22px;
pointer-events: none;
opacity: 0;
transform: translate(0, 0);
transition: opacity 200ms var(--ease-out), transform 560ms var(--ease-out-quint);
z-index: 5;
filter: drop-shadow(0 2px 4px oklch(0% 0 0 / 0.2));
}
.live-demo-cursor.is-visible { opacity: 1; }
.live-demo-cursor.is-click svg path {
transform-origin: 4px 4px;
animation: liveDemoCursorClick 220ms var(--ease-out);
}
@keyframes liveDemoCursorClick {
0%, 100% { transform: scale(1); }
40% { transform: scale(0.78); }
}
/* Global bar — the persistent dark pill at the bottom of every session. */
.live-demo-gbar {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
min-height: 36px;
padding: 0 4px;
background: oklch(14% 0 0);
color: oklch(92% 0 0);
border: 1px solid oklch(22% 0 0);
border-radius: 10px;
display: flex;
align-items: center;
gap: 2px;
font-family: var(--font-body);
font-size: 12px;
box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2);
z-index: 4;
}
.live-demo-gbar-brand {
font-family: var(--font-display);
font-size: 16px;
color: var(--color-accent);
padding: 0 10px;
}
.live-demo-gbar-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 10px;
background: transparent;
border: 0;
border-radius: 7px;
color: oklch(75% 0 0);
font: inherit;
cursor: pointer;
}
.live-demo-gbar-btn.is-active {
background: var(--color-accent-dim);
color: var(--color-accent);
}
.live-demo-gbar-dmd {
display: inline-grid;
grid-template: repeat(2, 1fr) / repeat(2, 1fr);
width: 12px;
height: 12px;
border-radius: 3px;
overflow: hidden;
}
.live-demo-gbar-dmd span:nth-child(1) { background: oklch(60% 0.25 350); }
.live-demo-gbar-dmd span:nth-child(2) { background: oklch(60% 0.15 45); }
.live-demo-gbar-dmd span:nth-child(3) { background: oklch(55% 0.12 250); }
.live-demo-gbar-dmd span:nth-child(4) { background: oklch(30% 0 0); }
.live-demo-gbar-divider {
width: 1px;
height: 18px;
background: oklch(28% 0 0);
margin: 0 4px;
}
.live-demo-gbar-x {
padding: 7px 10px;
background: transparent;
border: 0;
border-radius: 7px;
color: oklch(60% 0 0);
font-size: 13px;
cursor: pointer;
}
/* Contextual bar — LIGHT paper-backed pill that floats near the picked
element during a session. Matches live-browser.js's buildConfigureRow
look (dark command pill, transparent input, ×N count, magenta Go). */
.live-demo-ctx {
position: absolute;
left: 50%;
transform: translate(-50%, 6px);
padding: 6px;
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 10px;
box-shadow: 0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06);
font-family: var(--font-body);
font-size: 12px;
color: var(--color-ink);
display: none;
opacity: 0;
transition: opacity 260ms var(--ease-out), transform 260ms var(--ease-out);
z-index: 3;
max-width: 90%;
}
.live-demo-ctx[data-phase="configuring"],
.live-demo-ctx[data-phase="generating"],
.live-demo-ctx[data-phase="cycling"],
.live-demo-ctx[data-phase="accepted"] {
display: block;
opacity: 1;
transform: translate(-50%, 0);
}
.live-demo-ctx-row {
display: none;
align-items: center;
gap: 4px;
}
.live-demo-ctx[data-phase="configuring"] .live-demo-ctx-row--configure,
.live-demo-ctx[data-phase="generating"] .live-demo-ctx-row--generating,
.live-demo-ctx[data-phase="cycling"] .live-demo-ctx-row--cycling,
.live-demo-ctx[data-phase="accepted"] .live-demo-ctx-row--accepted {
display: flex;
}
/* Configure row: [delight ▾] [input] [×3] [Go →] */
.live-demo-ctx-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 10px;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 6px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
}
.live-demo-ctx-pill-caret { font-size: 9px; opacity: 0.7; margin-left: 2px; }
.live-demo-ctx-input {
display: inline-flex;
align-items: center;
flex: 1;
min-width: 180px;
padding: 5px 8px;
font-family: var(--font-body);
font-size: 12px;
color: var(--color-ink);
}
.live-demo-ctx-caret {
display: inline-block;
width: 1px;
height: 13px;
background: var(--color-ink);
margin-left: 2px;
animation: liveDemoCaret 1s steps(1) infinite;
}
@keyframes liveDemoCaret {
50% { opacity: 0; }
}
.live-demo-ctx-count {
padding: 4px 6px;
background: transparent;
border: 1px solid var(--color-mist);
border-radius: 5px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
color: var(--color-ash);
cursor: pointer;
}
.live-demo-ctx-go {
padding: 5px 12px;
background: var(--color-accent);
color: var(--color-paper);
border: 0;
border-radius: 6px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
/* Generating row */
.live-demo-ctx-row--generating {
gap: 10px;
padding: 4px 12px 4px 6px;
font-family: var(--font-body);
font-size: 12px;
color: var(--color-charcoal);
}
.live-demo-ctx-spinner {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1.5px solid var(--color-mist);
border-top-color: var(--color-accent);
animation: liveDemoSpin 700ms linear infinite;
}
@keyframes liveDemoSpin { to { transform: rotate(360deg); } }
/* Cycling row */
.live-demo-ctx-row--cycling { gap: 2px; padding: 2px; }
.live-demo-ctx-nav {
padding: 4px 10px;
background: transparent;
border: 0;
border-radius: 5px;
color: var(--color-charcoal);
font-size: 14px;
cursor: pointer;
}
.live-demo-ctx-counter {
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-ink);
padding: 0 6px;
min-width: 40px;
text-align: center;
}
.live-demo-ctx-divider {
width: 1px;
height: 18px;
background: var(--color-mist);
margin: 0 4px;
}
.live-demo-ctx-discard {
padding: 4px 10px;
background: transparent;
border: 0;
border-radius: 5px;
color: var(--color-ash);
font-size: 13px;
cursor: pointer;
}
.live-demo-ctx-accept {
padding: 5px 14px;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 6px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
/* Accepted confirmation row */
.live-demo-ctx-row--accepted {
gap: 8px;
padding: 6px 14px;
font-family: var(--font-body);
font-size: 12px;
color: oklch(45% 0.18 145);
}
/* Phase caption under the frame */
.live-demo-caption {
display: flex;
align-items: baseline;
gap: 10px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-ash);
}
.live-demo-caption::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-accent);
animation: liveDemoPulse 1.6s var(--ease-out) infinite;
}
@keyframes liveDemoPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}
.live-demo-caption-label { color: var(--color-ink); }
/* Supporting row under the demo */
.live-demo-support {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
align-self: stretch;
}
.live-demo-support-cell {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
padding-bottom: var(--spacing-md);
border-bottom: 1px dashed var(--color-mist);
}
.live-demo-support-cell:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.live-demo-support-k {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--color-accent);
}
.live-demo-support-v {
font-family: var(--font-body);
font-size: 14px;
line-height: 1.55;
color: var(--color-ink);
}
.live-demo-support-v code {
font-family: var(--font-mono);
font-size: 0.875em;
background: var(--color-mist);
padding: 1px 6px;
border-radius: 3px;
color: var(--color-ink);
}
@media (max-width: 760px) {
.live-demo-frame { aspect-ratio: 4 / 5; }
.live-demo-target { width: 90%; }
.live-demo-bar { font-size: 11px; min-height: 32px; }
.live-demo-support { grid-template-columns: 1fr; gap: var(--spacing-md); }
}
@media (prefers-reduced-motion: reduce) {
.live-demo-cursor,
.live-demo-outline,
.live-demo-variant,
.live-demo-annotations,
.live-demo-stroke path {
transition: none !important;
animation: none !important;
}
.live-demo-caption::before { animation: none; }
}
/* ============================================
LIVE MODE — skeleton page + richer variants
============================================ */
/* Skeleton elements behind the target card — suggest "this is a real page" */
.live-demo-skeleton {
position: absolute;
inset: 24px;
display: flex;
flex-direction: column;
gap: 14px;
opacity: 0.3;
pointer-events: none;
filter: blur(0.3px);
}
.live-demo-skel-nav {
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-mist);
}
.live-demo-skel-logo {
width: 22px;
height: 22px;
border-radius: 4px;
background: var(--color-charcoal);
}
.live-demo-skel-link {
width: 48px;
height: 8px;
border-radius: 2px;
background: var(--color-mist);
}
.live-demo-skel-cta {
margin-left: auto;
width: 72px;
height: 24px;
border-radius: 4px;
background: var(--color-charcoal);
}
.live-demo-skel-heading {
width: 60%;
height: 18px;
border-radius: 3px;
background: var(--color-mist);
margin-top: 20px;
}
.live-demo-skel-line {
height: 8px;
border-radius: 2px;
background: var(--color-mist);
}
.live-demo-skel-line--short { width: 40%; }
/* Target stays above skeleton */
.live-demo-target { position: relative; z-index: 1; }
/* ---- Card variants — sharper, more distinct ---- */
/* Variant 1: magazine column — italic serif, rule lines, off-white */
.live-demo-card--v1 {
background: var(--color-cream);
padding: 18px 22px;
border: 0;
border-top: 3px solid var(--color-ink);
border-radius: 0;
gap: 6px;
}
.live-demo-card--v1 .live-demo-card-kicker {
color: var(--color-accent);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.28em;
}
.live-demo-card--v1 h3 {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 28px;
line-height: 1.05;
letter-spacing: -0.01em;
margin-top: 4px;
}
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
.live-demo-card--v1 p {
font-family: var(--font-display);
font-style: italic;
font-size: 14px;
line-height: 1.55;
color: var(--color-charcoal);
}
.live-demo-card--v1 button {
font-family: var(--font-body);
align-self: flex-start;
margin-top: 6px;
background: transparent;
color: var(--color-ink);
border: 0;
border-bottom: 1.5px solid var(--color-ink);
padding: 4px 0;
border-radius: 0;
letter-spacing: 0.08em;
}
/* Variant 2: brutalist ticket — thick ink block, stamp glyph, mono */
.live-demo-card--v2 {
position: relative;
background: var(--color-ink);
color: var(--color-paper);
padding: 20px 24px;
border: 0;
border-radius: 0;
overflow: hidden;
gap: 8px;
}
.live-demo-card--v2::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
background: var(--color-accent);
}
.live-demo-card-stamp {
position: absolute;
top: 14px;
right: 18px;
font-size: 28px;
line-height: 1;
color: var(--color-accent);
transform: rotate(-8deg);
}
.live-demo-card--v2 .live-demo-card-kicker {
color: var(--color-accent);
letter-spacing: 0.3em;
font-weight: 600;
}
.live-demo-card--v2 h3 {
font-family: var(--font-body);
font-weight: 700;
font-size: 24px;
line-height: 1.1;
color: var(--color-paper);
letter-spacing: -0.02em;
}
.live-demo-card--v2 button {
font-family: var(--font-mono);
font-weight: 500;
align-self: flex-start;
margin-top: 6px;
background: var(--color-accent);
color: var(--color-paper);
border: 0;
border-radius: 0;
padding: 9px 14px;
letter-spacing: 0.04em;
text-transform: none;
font-size: 12px;
}
/* Variant 3: playful ticket — accent background, sticker stars, postcard vibe */
.live-demo-card--v3 {
position: relative;
background:
radial-gradient(circle at 20% 80%, oklch(92% 0.08 350) 0, transparent 45%),
var(--color-cream);
border: 1px dashed var(--color-accent);
border-radius: 10px;
padding: 22px 24px 20px;
gap: 8px;
}
.live-demo-card-sticker {
position: absolute;
top: 10px;
right: 14px;
display: flex;
gap: 3px;
color: var(--color-accent);
font-size: 14px;
line-height: 1;
transform: rotate(6deg);
}
.live-demo-card--v3 .live-demo-card-kicker {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 13px;
letter-spacing: 0.04em;
color: var(--color-accent-hover);
text-transform: none;
}
.live-demo-card--v3 h3 {
font-family: var(--font-display);
font-weight: 400;
font-size: 22px;
line-height: 1.2;
color: var(--color-ink);
}
.live-demo-card--v3 button {
display: inline-flex;
align-items: center;
gap: 6px;
align-self: flex-start;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 999px;
padding: 10px 18px;
font-family: var(--font-body);
font-weight: 500;
letter-spacing: 0.06em;
text-transform: none;
font-size: 12px;
margin-top: 6px;
}
+68 -19
View File
@@ -11,6 +11,7 @@
/* Section partials (landing page only) */
@import "./problem-section.css";
@import "./workflow.css";
@import "./live-mode.css";
@import "./gallery.css";
@import "./skill-demos.css";
@@ -1153,7 +1154,7 @@ code {
.language-content {
display: grid;
gap: var(--spacing-lg);
gap: var(--spacing-sm);
}
/* Grid and flex items default to min-width: auto, which lets them expand to
@@ -2442,28 +2443,76 @@ code {
padding: var(--spacing-lg);
}
.patterns-tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
.patterns-tabs-wrap {
position: relative;
margin-bottom: var(--spacing-md);
}
@media (max-width: 768px) {
.patterns-tabs {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
margin-left: calc(-1 * var(--spacing-lg));
margin-right: calc(-1 * var(--spacing-lg));
padding-left: var(--spacing-lg);
padding-right: var(--spacing-lg);
}
.patterns-tabs {
display: flex;
flex-wrap: nowrap;
gap: 6px;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
}
.patterns-tabs::-webkit-scrollbar {
display: none;
}
.patterns-tabs::-webkit-scrollbar {
display: none;
}
/* Edge fade only on sides where more content exists. data-scroll is set
by JS: start | middle | end | none. */
.patterns-tabs[data-scroll="start"] {
mask-image: linear-gradient(to right, black calc(100% - 40px), transparent);
-webkit-mask-image: linear-gradient(to right, black calc(100% - 40px), transparent);
}
.patterns-tabs[data-scroll="middle"] {
mask-image: linear-gradient(to right, transparent, black 32px, black calc(100% - 40px), transparent);
-webkit-mask-image: linear-gradient(to right, transparent, black 32px, black calc(100% - 40px), transparent);
}
.patterns-tabs[data-scroll="end"] {
mask-image: linear-gradient(to right, transparent, black 32px);
-webkit-mask-image: linear-gradient(to right, transparent, black 32px);
}
.patterns-tabs[data-scroll="none"] {
mask-image: none;
-webkit-mask-image: none;
}
/* Chevrons sit on the wrapper (outside the mask) so they're always fully
opaque, regardless of edge fade. Visible only when scroll can progress
in that direction. */
.patterns-tabs-wrap::before,
.patterns-tabs-wrap::after {
content: '';
position: absolute;
top: 50%;
width: 9px;
height: 9px;
border-top: 1.5px solid var(--color-ash);
border-right: 1.5px solid var(--color-ash);
pointer-events: none;
opacity: 0;
transition: opacity 0.18s ease;
}
.patterns-tabs-wrap::before {
left: 2px;
transform: translateY(-50%) rotate(-135deg);
}
.patterns-tabs-wrap::after {
right: 2px;
transform: translateY(-50%) rotate(45deg);
}
.patterns-tabs-wrap[data-scroll="start"]::after,
.patterns-tabs-wrap[data-scroll="middle"]::after { opacity: 1; }
.patterns-tabs-wrap[data-scroll="end"]::before,
.patterns-tabs-wrap[data-scroll="middle"]::before { opacity: 1; }
.patterns-tab {
scroll-snap-align: start;
flex-shrink: 0;
}
.patterns-tab {
+88 -816
View File
@@ -1472,11 +1472,27 @@
border-radius: 10px;
padding: var(--spacing-xl);
box-shadow: 0 1px 2px oklch(0.2 0 0 / 0.04);
/* All panels stack in one grid cell so the outgoing panel can cross-fade
out while the incoming one fades in. Container auto-sizes to the
tallest panel, so switching between tabs doesn't jump. */
display: grid;
grid-template-areas: "stack";
}
.why-panel {
display: none;
animation: whyFadeIn 260ms var(--ease-out);
.why-panel,
.why-panel[hidden] {
/* [hidden] attribute on initial markup would force display:none and skip
the crossfade; keep the element rendered so opacity/transform can run. */
display: block;
grid-area: stack;
opacity: 0;
visibility: hidden;
transform: translateY(8px);
transition:
opacity 650ms var(--ease-out),
transform 800ms var(--ease-out),
visibility 0s linear 650ms;
pointer-events: none;
}
.why-panel-title,
@@ -1486,7 +1502,14 @@
}
.why-panel.is-active {
display: block;
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
transition:
opacity 650ms var(--ease-out),
transform 800ms var(--ease-out),
visibility 0s linear 0s;
}
.why-panel-title {
@@ -1557,13 +1580,12 @@
.why-panel-meta a:hover { color: var(--color-accent); }
@keyframes whyFadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.why-panel { animation: none; }
.why-panel,
.why-panel.is-active {
transition: none;
transform: none;
}
}
@media (max-width: 900px) {
@@ -1613,7 +1635,7 @@
}
.why-tab.is-active.is-cycling .why-tab-progress {
animation: whyTabProgress var(--why-cycle-ms, 7000ms) cubic-bezier(0.4, 0, 0.2, 1) forwards;
animation: whyTabProgress var(--why-cycle-ms, 7000ms) linear forwards;
}
@keyframes whyTabProgress {
@@ -2182,12 +2204,50 @@
/* ─ Panel 01: PRODUCT.md file view ─ */
.why-visual--productmd {
padding: 0;
background: var(--color-paper);
background: transparent;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--spacing-lg);
align-items: start;
}
@media (max-width: 780px) {
.why-visual--productmd {
grid-template-columns: 1fr;
}
}
.why-productmd-file {
flex: 1;
display: flex;
flex-direction: column;
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 8px;
overflow: hidden;
min-width: 0;
}
.why-productmd-commands {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 4px;
min-width: 160px;
}
.why-productmd-commands-label {
font-family: var(--font-mono);
font-size: 9.5px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--color-ash);
margin-bottom: 4px;
}
.why-productmd-commands code {
font-family: var(--font-mono);
font-size: 12px;
color: var(--color-ink);
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 5px;
padding: 5px 9px;
white-space: nowrap;
}
.why-productmd-header {
display: flex;
@@ -2701,590 +2761,6 @@
margin-top: -2px;
}
/* ============================================
LIVE MODE — interactive demo loop
============================================ */
.live-section {
padding: var(--spacing-2xl) 0;
border-top: 1px solid var(--color-mist);
}
.live-content .section-lead {
max-width: 64ch;
margin-bottom: var(--spacing-xl);
}
.live-demo {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
gap: var(--spacing-xl);
align-items: start;
margin-bottom: var(--spacing-xl);
}
.live-demo-frame-col {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
min-width: 0;
}
@media (max-width: 920px) {
.live-demo { grid-template-columns: 1fr; }
}
.live-demo-frame {
position: relative;
background: var(--color-cream);
border: 1px solid var(--color-mist);
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16 / 9;
max-width: 960px;
box-shadow: 0 20px 50px oklch(0% 0 0 / 0.08);
user-select: none;
}
.live-demo-chrome {
display: flex;
align-items: center;
gap: 7px;
padding: 10px 14px;
border-bottom: 1px solid var(--color-mist);
background: var(--color-paper);
}
.live-demo-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--color-mist);
}
.live-demo-url {
margin-left: 14px;
padding: 4px 14px;
background: var(--color-cream);
border: 1px solid var(--color-mist);
border-radius: 5px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-charcoal);
}
.live-demo-stage {
position: relative;
height: calc(100% - 42px - 60px); /* minus chrome + bar */
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
overflow: hidden;
}
/* Target card */
.live-demo-target {
position: relative;
width: min(360px, 80%);
min-height: 200px;
}
.live-demo-variant {
position: absolute;
inset: 0;
opacity: 0;
transform: translateY(8px) scale(0.99);
transition: opacity 280ms var(--ease-out), transform 280ms var(--ease-out);
pointer-events: none;
}
.live-demo-variant.is-active {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.live-demo-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 22px 24px;
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 8px;
height: 100%;
box-sizing: border-box;
}
.live-demo-card-kicker {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-ash);
}
.live-demo-card h3 {
font-family: var(--font-display);
font-weight: 400;
font-size: 20px;
line-height: 1.2;
color: var(--color-ink);
margin: 0;
}
.live-demo-card p {
font-family: var(--font-body);
font-size: 13px;
line-height: 1.5;
color: var(--color-charcoal);
margin: 0;
}
.live-demo-card button {
align-self: flex-start;
margin-top: auto;
font-family: var(--font-body);
font-weight: 500;
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 9px 16px;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 0;
cursor: pointer;
}
/* Per-variant look */
.live-demo-card--plain {}
.live-demo-card--v1 {
background: var(--color-cream);
}
.live-demo-card--v1 h3 { font-style: italic; }
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
.live-demo-card--v2 {
background: var(--color-paper);
border: 1px dashed var(--color-accent);
}
.live-demo-card--v2 .live-demo-card-kicker { color: var(--color-accent); }
.live-demo-card--v2 h3 { font-family: var(--font-display); font-style: italic; }
.live-demo-card--v2 button {
background: var(--color-accent);
}
.live-demo-card--v3 {
background: oklch(96% 0.02 350);
border: 1px solid var(--color-accent-soft);
}
.live-demo-card--v3 h3 em { color: var(--color-accent); font-style: italic; }
.live-demo-card--v3 .live-demo-card-kicker { color: var(--color-accent-hover); }
/* Highlight outline over the target */
.live-demo-outline {
position: absolute;
border: 2px solid var(--color-accent);
border-radius: 8px;
pointer-events: none;
opacity: 0;
transition: opacity 200ms var(--ease-out), top 320ms var(--ease-out), left 320ms var(--ease-out), width 320ms var(--ease-out), height 320ms var(--ease-out);
box-shadow: 0 0 0 4px var(--color-accent-dim);
}
.live-demo-outline.is-visible {
opacity: 1;
}
/* Annotations */
.live-demo-annotations {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0;
transition: opacity 280ms var(--ease-out);
color: var(--color-accent);
}
.live-demo-annotations.is-visible { opacity: 1; }
.live-demo-stroke {
position: absolute;
width: 280px;
height: 56px;
top: 50%;
left: 50%;
transform: translate(-50%, -24px);
pointer-events: none;
}
.live-demo-stroke path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.live-demo-annotations.is-visible .live-demo-stroke path {
animation: liveDemoStroke 800ms var(--ease-out) forwards;
}
@keyframes liveDemoStroke {
to { stroke-dashoffset: 0; }
}
.live-demo-comment {
position: absolute;
top: 56%;
left: 50%;
transform: translate(-50%, 0);
padding: 5px 10px;
background: var(--color-ink);
color: var(--color-paper);
font-family: var(--font-mono);
font-size: 11px;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
transition: opacity 200ms var(--ease-out);
}
.live-demo-annotations.is-comment-visible .live-demo-comment { opacity: 1; }
/* Simulated cursor */
.live-demo-cursor {
position: absolute;
top: 0;
left: 0;
width: 18px;
height: 22px;
pointer-events: none;
opacity: 0;
transform: translate(0, 0);
transition: opacity 200ms var(--ease-out), transform 560ms var(--ease-out-quint);
z-index: 5;
filter: drop-shadow(0 2px 4px oklch(0% 0 0 / 0.2));
}
.live-demo-cursor.is-visible { opacity: 1; }
.live-demo-cursor.is-click svg path {
transform-origin: 4px 4px;
animation: liveDemoCursorClick 220ms var(--ease-out);
}
@keyframes liveDemoCursorClick {
0%, 100% { transform: scale(1); }
40% { transform: scale(0.78); }
}
/* Global bar — the persistent dark pill at the bottom of every session. */
.live-demo-gbar {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
min-height: 36px;
padding: 0 4px;
background: oklch(14% 0 0);
color: oklch(92% 0 0);
border: 1px solid oklch(22% 0 0);
border-radius: 10px;
display: flex;
align-items: center;
gap: 2px;
font-family: var(--font-body);
font-size: 12px;
box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2);
z-index: 4;
}
.live-demo-gbar-brand {
font-family: var(--font-display);
font-size: 16px;
color: var(--color-accent);
padding: 0 10px;
}
.live-demo-gbar-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 10px;
background: transparent;
border: 0;
border-radius: 7px;
color: oklch(75% 0 0);
font: inherit;
cursor: pointer;
}
.live-demo-gbar-btn.is-active {
background: var(--color-accent-dim);
color: var(--color-accent);
}
.live-demo-gbar-dmd {
display: inline-grid;
grid-template: repeat(2, 1fr) / repeat(2, 1fr);
width: 12px;
height: 12px;
border-radius: 3px;
overflow: hidden;
}
.live-demo-gbar-dmd span:nth-child(1) { background: oklch(60% 0.25 350); }
.live-demo-gbar-dmd span:nth-child(2) { background: oklch(60% 0.15 45); }
.live-demo-gbar-dmd span:nth-child(3) { background: oklch(55% 0.12 250); }
.live-demo-gbar-dmd span:nth-child(4) { background: oklch(30% 0 0); }
.live-demo-gbar-divider {
width: 1px;
height: 18px;
background: oklch(28% 0 0);
margin: 0 4px;
}
.live-demo-gbar-x {
padding: 7px 10px;
background: transparent;
border: 0;
border-radius: 7px;
color: oklch(60% 0 0);
font-size: 13px;
cursor: pointer;
}
/* Contextual bar — LIGHT paper-backed pill that floats near the picked
element during a session. Matches live-browser.js's buildConfigureRow
look (dark command pill, transparent input, ×N count, magenta Go). */
.live-demo-ctx {
position: absolute;
left: 50%;
transform: translate(-50%, 6px);
padding: 6px;
background: var(--color-paper);
border: 1px solid var(--color-mist);
border-radius: 10px;
box-shadow: 0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06);
font-family: var(--font-body);
font-size: 12px;
color: var(--color-ink);
display: none;
opacity: 0;
transition: opacity 260ms var(--ease-out), transform 260ms var(--ease-out);
z-index: 3;
max-width: 90%;
}
.live-demo-ctx[data-phase="configuring"],
.live-demo-ctx[data-phase="generating"],
.live-demo-ctx[data-phase="cycling"],
.live-demo-ctx[data-phase="accepted"] {
display: block;
opacity: 1;
transform: translate(-50%, 0);
}
.live-demo-ctx-row {
display: none;
align-items: center;
gap: 4px;
}
.live-demo-ctx[data-phase="configuring"] .live-demo-ctx-row--configure,
.live-demo-ctx[data-phase="generating"] .live-demo-ctx-row--generating,
.live-demo-ctx[data-phase="cycling"] .live-demo-ctx-row--cycling,
.live-demo-ctx[data-phase="accepted"] .live-demo-ctx-row--accepted {
display: flex;
}
/* Configure row: [delight ▾] [input] [×3] [Go →] */
.live-demo-ctx-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 10px;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 6px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
}
.live-demo-ctx-pill-caret { font-size: 9px; opacity: 0.7; margin-left: 2px; }
.live-demo-ctx-input {
display: inline-flex;
align-items: center;
flex: 1;
min-width: 180px;
padding: 5px 8px;
font-family: var(--font-body);
font-size: 12px;
color: var(--color-ink);
}
.live-demo-ctx-caret {
display: inline-block;
width: 1px;
height: 13px;
background: var(--color-ink);
margin-left: 2px;
animation: liveDemoCaret 1s steps(1) infinite;
}
@keyframes liveDemoCaret {
50% { opacity: 0; }
}
.live-demo-ctx-count {
padding: 4px 6px;
background: transparent;
border: 1px solid var(--color-mist);
border-radius: 5px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
color: var(--color-ash);
cursor: pointer;
}
.live-demo-ctx-go {
padding: 5px 12px;
background: var(--color-accent);
color: var(--color-paper);
border: 0;
border-radius: 6px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
/* Generating row */
.live-demo-ctx-row--generating {
gap: 10px;
padding: 4px 12px 4px 6px;
font-family: var(--font-body);
font-size: 12px;
color: var(--color-charcoal);
}
.live-demo-ctx-spinner {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1.5px solid var(--color-mist);
border-top-color: var(--color-accent);
animation: liveDemoSpin 700ms linear infinite;
}
@keyframes liveDemoSpin { to { transform: rotate(360deg); } }
/* Cycling row */
.live-demo-ctx-row--cycling { gap: 2px; padding: 2px; }
.live-demo-ctx-nav {
padding: 4px 10px;
background: transparent;
border: 0;
border-radius: 5px;
color: var(--color-charcoal);
font-size: 14px;
cursor: pointer;
}
.live-demo-ctx-counter {
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-ink);
padding: 0 6px;
min-width: 40px;
text-align: center;
}
.live-demo-ctx-divider {
width: 1px;
height: 18px;
background: var(--color-mist);
margin: 0 4px;
}
.live-demo-ctx-discard {
padding: 4px 10px;
background: transparent;
border: 0;
border-radius: 5px;
color: var(--color-ash);
font-size: 13px;
cursor: pointer;
}
.live-demo-ctx-accept {
padding: 5px 14px;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 6px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
/* Accepted confirmation row */
.live-demo-ctx-row--accepted {
gap: 8px;
padding: 6px 14px;
font-family: var(--font-body);
font-size: 12px;
color: oklch(45% 0.18 145);
}
/* Phase caption under the frame */
.live-demo-caption {
display: flex;
align-items: baseline;
gap: 10px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-ash);
}
.live-demo-caption::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-accent);
animation: liveDemoPulse 1.6s var(--ease-out) infinite;
}
@keyframes liveDemoPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}
.live-demo-caption-label { color: var(--color-ink); }
/* Supporting row under the demo */
.live-demo-support {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
align-self: stretch;
}
.live-demo-support-cell {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
padding-bottom: var(--spacing-md);
border-bottom: 1px dashed var(--color-mist);
}
.live-demo-support-cell:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.live-demo-support-k {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--color-accent);
}
.live-demo-support-v {
font-family: var(--font-body);
font-size: 14px;
line-height: 1.55;
color: var(--color-ink);
}
.live-demo-support-v code {
font-family: var(--font-mono);
font-size: 0.875em;
background: var(--color-mist);
padding: 1px 6px;
border-radius: 3px;
color: var(--color-ink);
}
@media (max-width: 760px) {
.live-demo-frame { aspect-ratio: 4 / 5; }
.live-demo-target { width: 90%; }
.live-demo-bar { font-size: 11px; min-height: 32px; }
.live-demo-support { grid-template-columns: 1fr; gap: var(--spacing-md); }
}
@media (prefers-reduced-motion: reduce) {
.live-demo-cursor,
.live-demo-outline,
.live-demo-variant,
.live-demo-annotations,
.live-demo-stroke path {
transition: none !important;
animation: none !important;
}
.live-demo-caption::before { animation: none; }
}
/* ============================================
LANGUAGE section — Palette / Periodic tabs
@@ -3295,7 +2771,6 @@
align-items: flex-end;
justify-content: space-between;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.language-intro-row .section-lead {
@@ -3449,224 +2924,6 @@
}
}
/* ============================================
LIVE MODE — skeleton page + richer variants
============================================ */
/* Skeleton elements behind the target card — suggest "this is a real page" */
.live-demo-skeleton {
position: absolute;
inset: 24px;
display: flex;
flex-direction: column;
gap: 14px;
opacity: 0.3;
pointer-events: none;
filter: blur(0.3px);
}
.live-demo-skel-nav {
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-mist);
}
.live-demo-skel-logo {
width: 22px;
height: 22px;
border-radius: 4px;
background: var(--color-charcoal);
}
.live-demo-skel-link {
width: 48px;
height: 8px;
border-radius: 2px;
background: var(--color-mist);
}
.live-demo-skel-cta {
margin-left: auto;
width: 72px;
height: 24px;
border-radius: 4px;
background: var(--color-charcoal);
}
.live-demo-skel-heading {
width: 60%;
height: 18px;
border-radius: 3px;
background: var(--color-mist);
margin-top: 20px;
}
.live-demo-skel-line {
height: 8px;
border-radius: 2px;
background: var(--color-mist);
}
.live-demo-skel-line--short { width: 40%; }
/* Target stays above skeleton */
.live-demo-target { position: relative; z-index: 1; }
/* ---- Card variants — sharper, more distinct ---- */
/* Variant 1: magazine column — italic serif, rule lines, off-white */
.live-demo-card--v1 {
background: var(--color-cream);
padding: 18px 22px;
border: 0;
border-top: 3px solid var(--color-ink);
border-radius: 0;
gap: 6px;
}
.live-demo-card--v1 .live-demo-card-kicker {
color: var(--color-accent);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.28em;
}
.live-demo-card--v1 h3 {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 28px;
line-height: 1.05;
letter-spacing: -0.01em;
margin-top: 4px;
}
.live-demo-card--v1 h3 em { color: var(--color-accent); font-style: italic; }
.live-demo-card--v1 p {
font-family: var(--font-display);
font-style: italic;
font-size: 14px;
line-height: 1.55;
color: var(--color-charcoal);
}
.live-demo-card--v1 button {
font-family: var(--font-body);
align-self: flex-start;
margin-top: 6px;
background: transparent;
color: var(--color-ink);
border: 0;
border-bottom: 1.5px solid var(--color-ink);
padding: 4px 0;
border-radius: 0;
letter-spacing: 0.08em;
}
/* Variant 2: brutalist ticket — thick ink block, stamp glyph, mono */
.live-demo-card--v2 {
position: relative;
background: var(--color-ink);
color: var(--color-paper);
padding: 20px 24px;
border: 0;
border-radius: 0;
overflow: hidden;
gap: 8px;
}
.live-demo-card--v2::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
background: var(--color-accent);
}
.live-demo-card-stamp {
position: absolute;
top: 14px;
right: 18px;
font-size: 28px;
line-height: 1;
color: var(--color-accent);
transform: rotate(-8deg);
}
.live-demo-card--v2 .live-demo-card-kicker {
color: var(--color-accent);
letter-spacing: 0.3em;
font-weight: 600;
}
.live-demo-card--v2 h3 {
font-family: var(--font-body);
font-weight: 700;
font-size: 24px;
line-height: 1.1;
color: var(--color-paper);
letter-spacing: -0.02em;
}
.live-demo-card--v2 button {
font-family: var(--font-mono);
font-weight: 500;
align-self: flex-start;
margin-top: 6px;
background: var(--color-accent);
color: var(--color-paper);
border: 0;
border-radius: 0;
padding: 9px 14px;
letter-spacing: 0.04em;
text-transform: none;
font-size: 12px;
}
/* Variant 3: playful ticket — accent background, sticker stars, postcard vibe */
.live-demo-card--v3 {
position: relative;
background:
radial-gradient(circle at 20% 80%, oklch(92% 0.08 350) 0, transparent 45%),
var(--color-cream);
border: 1px dashed var(--color-accent);
border-radius: 10px;
padding: 22px 24px 20px;
gap: 8px;
}
.live-demo-card-sticker {
position: absolute;
top: 10px;
right: 14px;
display: flex;
gap: 3px;
color: var(--color-accent);
font-size: 14px;
line-height: 1;
transform: rotate(6deg);
}
.live-demo-card--v3 .live-demo-card-kicker {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 13px;
letter-spacing: 0.04em;
color: var(--color-accent-hover);
text-transform: none;
}
.live-demo-card--v3 h3 {
font-family: var(--font-display);
font-weight: 400;
font-size: 22px;
line-height: 1.2;
color: var(--color-ink);
}
.live-demo-card--v3 button {
display: inline-flex;
align-items: center;
gap: 6px;
align-self: flex-start;
background: var(--color-ink);
color: var(--color-paper);
border: 0;
border-radius: 999px;
padding: 10px 18px;
font-family: var(--font-body);
font-weight: 500;
letter-spacing: 0.06em;
text-transform: none;
font-size: 12px;
margin-top: 6px;
}
/* ============================================
ANTIDOTE — visual-first wall of three
============================================ */
@@ -4014,6 +3271,21 @@
max-width: none;
}
.gallery-stack-link {
display: inline-flex;
align-self: center;
margin-top: var(--spacing-sm);
font-family: var(--font-body);
font-size: 0.82rem;
color: var(--color-ink);
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-color: var(--color-accent);
transition: color 0.12s ease;
}
.gallery-stack-link:hover { color: var(--color-accent); }
@media (max-width: 860px) {
.why-visual--antidote { grid-template-columns: 1fr; }
}
+13 -14
View File
@@ -60,7 +60,7 @@
<a href="/" data-nav="home" aria-current="page">Home</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
<a href="/docs/live" data-nav="live">Live</a>
<a href="/live-mode" data-nav="live">Live</a>
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
@@ -288,13 +288,14 @@
<span class="why-productmd-v">Purple gradients. Glassmorphism. "Boost your productivity."</span>
</div>
</div>
<div class="why-productmd-footer">
<span class="why-productmd-footer-dot"></span>
Every command reads this before writing a line of code.
</div>
</div>
<aside class="why-productmd-commands">
<span class="why-productmd-commands-label">Commands</span>
<code>/impeccable teach</code>
<code>/impeccable document</code>
<code>/impeccable shape</code>
</aside>
</div>
<p class="why-panel-meta"><code>/impeccable teach</code> &middot; <code>/impeccable document</code> &middot; <code>/impeccable shape</code></p>
</article>
<article class="why-panel why-panel--antidote" id="why-panel-1" role="tabpanel" data-index="1" hidden>
@@ -336,9 +337,9 @@
<span class="gallery-stack-label">Bad Contrast</span>
</a>
</div>
<a class="gallery-stack-link" href="/anti-patterns">Browse the full catalog &rarr;</a>
</div>
</div>
<p class="why-panel-meta"><a href="/anti-patterns">Browse the full catalog &rarr;</a></p>
</article>
<article class="why-panel" id="why-panel-2" role="tabpanel" data-index="2" hidden>
@@ -370,17 +371,17 @@
<article class="why-panel" id="why-panel-3" role="tabpanel" data-index="3" hidden>
<h3 class="why-panel-title">Brand work is not product UI.</h3>
<p class="why-panel-body">Most design skills, including Anthropic's own frontend-design skill, collapse everything into one vocabulary. But a landing page and a dashboard live by contradictory rules. Impeccable carries two registers: <em>brand</em> (design IS the product: marketing, portfolios, editorial) and <em>product</em> (design SERVES the product: app UI, tools, dashboards). Register-aware commands (<code>typeset</code>, <code>animate</code>, <code>colorize</code>, <code>bolder</code>, <code>quieter</code>, <code>layout</code>, <code>delight</code>) adjust their choices to match.</p>
<p class="why-panel-body">Most design skills, including Anthropic's own frontend-design skill, collapse everything into one vocabulary. But a landing page and a dashboard live by contradictory rules. Impeccable runs in two modes: <em>brand</em> (design IS the product: marketing, portfolios, editorial) and <em>product</em> (design SERVES the product: app UI, tools, dashboards). Commands like <code>typeset</code>, <code>animate</code>, <code>colorize</code>, <code>bolder</code>, <code>quieter</code>, <code>layout</code>, and <code>delight</code> adjust their choices to match.</p>
<div class="why-visual why-visual--registers">
<div class="why-register why-register--brand">
<span class="why-register-label">Brand register</span>
<span class="why-register-label">Brand mode</span>
<div class="why-register-mock why-register-mock--brand">
<span class="why-brand-hero-mono">Issue 04</span>
<div class="why-brand-hero-title"><em>Shape</em> the story.</div>
</div>
</div>
<div class="why-register why-register--product">
<span class="why-register-label">Product register</span>
<span class="why-register-label">Product mode</span>
<div class="why-register-mock why-register-mock--product">
<div class="why-product-row"><span class="why-product-k">Users</span><span class="why-product-v">12,482</span></div>
<div class="why-product-row"><span class="why-product-k">Active now</span><span class="why-product-v">1,207</span></div>
@@ -388,7 +389,7 @@
</div>
</div>
</div>
<p class="why-panel-meta">The register is inferred from your task and confirmed in <code>PRODUCT.md</code>.</p>
<p class="why-panel-meta">The mode is inferred from your task and confirmed in <code>PRODUCT.md</code>.</p>
</article>
<article class="why-panel" id="why-panel-4" role="tabpanel" data-index="4" hidden>
@@ -424,7 +425,6 @@
</div>
</div>
</div>
<p class="why-panel-meta">Works in Claude Code, Cursor, Codex, Gemini CLI, Antigravity, Kiro, OpenCode, and more.</p>
</article>
<article class="why-panel" id="why-panel-5" role="tabpanel" data-index="5" hidden>
@@ -471,7 +471,6 @@
</div>
<div class="why-dm-foot">Readable by every DESIGN.md-aware tool.</div>
</div>
<p class="why-panel-meta">Spec-compliant. Interoperable. Not a proprietary sidecar.</p>
</article>
<article class="why-panel" id="why-panel-6" role="tabpanel" data-index="6" hidden>
@@ -1143,7 +1142,7 @@
<nav class="footer-links" aria-label="Footer">
<a href="/docs">Docs</a>
<a href="/anti-patterns">Anti-Patterns</a>
<a href="/docs/live">Live Mode</a>
<a href="/live-mode">Live Mode</a>
<a href="/visual-mode">Overlay</a>
<a href="#language">Commands</a>
<a href="/privacy">Privacy</a>
+395
View File
@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Mode | Impeccable</title>
<meta name="description" content="Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/live-mode">
<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">
<link rel="stylesheet" href="../css/live-mode.css">
<script type="module">
import { initLiveDemo } from "../js/components/live-demo.js";
document.addEventListener("DOMContentLoaded", initLiveDemo);
</script>
</head>
<body class="sub-page live-mode-page-body">
<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="/docs" data-nav="docs">Docs</a>
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
<a href="/live-mode" data-nav="live" aria-current="page">Live</a>
<a href="/visual-mode" data-nav="visual-mode">Overlay</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="live-mode-page">
<header class="live-mode-page-header">
<p class="live-mode-page-eyebrow">New in v3.0 <span class="live-mode-page-eyebrow-badge">Beta</span></p>
<h1 class="live-mode-page-title">Live Mode</h1>
<p class="live-mode-page-lede">Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.</p>
<div class="live-mode-start" aria-label="Start live mode command">
<span class="live-mode-start-prompt">$</span>
<code class="live-mode-start-cmd">/impeccable live</code>
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
</button>
</div>
</header>
<section class="live-mode-demo-wrap" aria-label="Live Mode interactive demo">
<div class="live-demo" id="live-demo" aria-label="Live Mode interactive demo loop">
<div class="live-demo-frame-col"><div class="live-demo-frame">
<div class="live-demo-chrome">
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-url">localhost:3000</span>
</div>
<div class="live-demo-stage">
<div class="live-demo-skeleton" aria-hidden="true">
<div class="live-demo-skel-nav">
<span class="live-demo-skel-logo"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-cta"></span>
</div>
<div class="live-demo-skel-heading"></div>
<div class="live-demo-skel-line"></div>
<div class="live-demo-skel-line live-demo-skel-line--short"></div>
</div>
<div class="live-demo-target" data-demo-target>
<div class="live-demo-variant is-active" data-variant="original">
<div class="live-demo-card live-demo-card--plain">
<span class="live-demo-card-kicker">Newsletter</span>
<h3>Subscribe for updates</h3>
<p>Monthly-ish design notes.</p>
<button type="button">Subscribe</button>
</div>
</div>
<div class="live-demo-variant" data-variant="1">
<div class="live-demo-card live-demo-card--v1">
<span class="live-demo-card-kicker">No. 04</span>
<h3>Letters, <em>occasionally</em>.</h3>
<p>A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
<button type="button">Send me one</button>
</div>
</div>
<div class="live-demo-variant" data-variant="2">
<div class="live-demo-card live-demo-card--v2">
<div class="live-demo-card-stamp"></div>
<span class="live-demo-card-kicker">Dispatch</span>
<h3>Design&nbsp;notes, <br>every&nbsp;other<br>Thursday.</h3>
<button type="button">Join the list →</button>
</div>
</div>
<div class="live-demo-variant" data-variant="3">
<div class="live-demo-card live-demo-card--v3">
<div class="live-demo-card-sticker"><span>&star;</span><span>&star;</span><span>&star;</span></div>
<span class="live-demo-card-kicker">Field Notes</span>
<h3>A monthly letter, for people who still read email for pleasure.</h3>
<button type="button">Receive the letter <span aria-hidden="true"></span></button>
</div>
</div>
</div>
<div class="live-demo-outline" data-demo-outline aria-hidden="true"></div>
<div class="live-demo-annotations" data-demo-annotations aria-hidden="true">
<svg class="live-demo-stroke" viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
<path d="M 10,40 Q 60,10 110,38 T 210,32 T 290,20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" pathLength="1"/>
</svg>
<div class="live-demo-comment">more playful</div>
</div>
<div class="live-demo-ctx" data-demo-ctx data-phase="hidden">
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
<button type="button" class="live-demo-ctx-pill" data-demo-ctx-pill>
<span data-demo-cmd-name>delight</span>
<span class="live-demo-ctx-pill-caret" aria-hidden="true"></span>
</button>
<span class="live-demo-ctx-input" data-demo-input>
<span data-demo-input-text></span><span class="live-demo-ctx-caret"></span>
</span>
<button type="button" class="live-demo-ctx-count">×3</button>
<button type="button" class="live-demo-ctx-go" data-demo-go>Go <span aria-hidden="true"></span></button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--generating">
<span class="live-demo-ctx-spinner" aria-hidden="true"></span>
<span>Generating variants…</span>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--cycling">
<button type="button" class="live-demo-ctx-nav" aria-label="Previous variant"></button>
<span class="live-demo-ctx-counter" data-demo-counter>1 / 3</span>
<button type="button" class="live-demo-ctx-nav" aria-label="Next variant"></button>
<span class="live-demo-ctx-divider"></span>
<button type="button" class="live-demo-ctx-discard" aria-label="Discard"></button>
<button type="button" class="live-demo-ctx-accept" data-demo-accept>Accept</button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--accepted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span>Variant 3 written to source</span>
</div>
</div>
<div class="live-demo-cursor" data-demo-cursor aria-hidden="true">
<svg width="18" height="22" viewBox="0 0 18 22" fill="none">
<path d="M1 1 L1 17 L5 13 L8 20 L11 19 L7.5 12 L13 12 Z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="live-demo-gbar" data-demo-gbar>
<span class="live-demo-gbar-brand">/</span>
<button type="button" class="live-demo-gbar-btn is-active" data-demo-gbar-pick>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
<span>Pick</span>
</button>
<button type="button" class="live-demo-gbar-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button type="button" class="live-demo-gbar-btn">
<span class="live-demo-gbar-dmd" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
</button>
<span class="live-demo-gbar-divider"></span>
<button type="button" class="live-demo-gbar-x" aria-label="Exit live mode"></button>
</div>
</div>
</div>
</div>
<p class="live-mode-demo-caption">Click the frame or scroll it into view to start the loop. Respects <code>prefers-reduced-motion</code>.</p>
</section>
<section class="live-mode-stages" aria-label="What happens in a live mode session">
<h2 class="live-mode-stages-title">What happens, in three moves</h2>
<div class="live-mode-stages-grid">
<article class="live-mode-stage">
<span class="live-mode-stage-num">01 &middot; Pick</span>
<h3 class="live-mode-stage-name">Point at what bugs you</h3>
<p class="live-mode-stage-desc">Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-picker-row" style="min-height:72px;padding:10px">
<div class="docs-viz-picker-target" style="font-size:12px;padding:6px 12px">
Newsletter card
<span class="docs-viz-picker-pin" style="width:18px;height:18px;font-size:9px">1</span>
</div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">02 &middot; Generate</span>
<h3 class="live-mode-stage-name">Three genuinely different takes</h3>
<p class="live-mode-stage-desc">Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-variants" style="width:100%;gap:4px">
<div class="docs-viz-variant docs-viz-variant--v1" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">No.04</span></div>
<div class="docs-viz-variant docs-viz-variant--v2 is-active" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Dispatch</span></div>
<div class="docs-viz-variant docs-viz-variant--v3" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Field</span></div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">03 &middot; Accept</span>
<h3 class="live-mode-stage-name">Lands in real source</h3>
<p class="live-mode-stage-desc">The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.</p>
<div class="live-mode-stage-viz">
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
</div>
</article>
</div>
</section>
<section class="live-mode-pathways" aria-label="Where to go next">
<h2 class="live-mode-pathways-title">Where next</h2>
<div class="live-mode-pathways-grid">
<a class="live-mode-pathway" href="/tutorials/iterate-live">
<span class="live-mode-pathway-kind">Tutorial</span>
<h3 class="live-mode-pathway-title">Walk it step by step</h3>
<p class="live-mode-pathway-desc">A ten-minute walkthrough from first run to accepted variant. Covers CSP patching, the picker actions, and the fallback flow for generated files.</p>
<span class="live-mode-pathway-cta">Open the tutorial &rarr;</span>
</a>
<a class="live-mode-pathway" href="/docs/live">
<span class="live-mode-pathway-kind">Reference</span>
<h3 class="live-mode-pathway-title">Full command reference</h3>
<p class="live-mode-pathway-desc">Everything your AI harness reads when <code>/impeccable live</code> runs: the poll loop, the wrap/accept helpers, the CSP templates, and every event shape.</p>
<span class="live-mode-pathway-cta">Read the reference &rarr;</span>
</a>
<a class="live-mode-pathway" href="/#downloads">
<span class="live-mode-pathway-kind">Install</span>
<h3 class="live-mode-pathway-title">Get Impeccable set up</h3>
<p class="live-mode-pathway-desc">Install the skill and CLI once, then run <code>/impeccable live</code> from your AI harness. Works with Claude Code, Cursor, Codex, Gemini, and more.</p>
<span class="live-mode-pathway-cta">See the install steps &rarr;</span>
</a>
</div>
</section>
<section class="live-mode-frameworks" aria-label="Supported frameworks">
<span class="live-mode-frameworks-label">Supported dev servers</span>
<ul class="live-mode-frameworks-list">
<li>Vite</li>
<li>Next.js (incl. monorepos)</li>
<li>SvelteKit</li>
<li>Astro</li>
<li>Nuxt</li>
<li>Bun</li>
<li>Plain static HTML</li>
</ul>
</section>
</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>
+270 -1
View File
@@ -597,6 +597,249 @@ ${specimenCards}
</div>`;
}
/**
* Render the animated Live Mode demo block. HTML structure matches the
* homepage #live-demo exactly so public/js/components/live-demo.js can
* drive it without modification. The CSS (.live-demo-*) lives in
* public/css/live-mode.css, imported by main.css and also loaded on this
* page via extraHead.
*/
function renderLiveModeDemo() {
return `
<div class="live-demo" id="live-demo" aria-label="Live Mode interactive demo loop">
<div class="live-demo-frame-col"><div class="live-demo-frame">
<div class="live-demo-chrome">
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-url">localhost:3000</span>
</div>
<div class="live-demo-stage">
<div class="live-demo-skeleton" aria-hidden="true">
<div class="live-demo-skel-nav">
<span class="live-demo-skel-logo"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-cta"></span>
</div>
<div class="live-demo-skel-heading"></div>
<div class="live-demo-skel-line"></div>
<div class="live-demo-skel-line live-demo-skel-line--short"></div>
</div>
<div class="live-demo-target" data-demo-target>
<div class="live-demo-variant is-active" data-variant="original">
<div class="live-demo-card live-demo-card--plain">
<span class="live-demo-card-kicker">Newsletter</span>
<h3>Subscribe for updates</h3>
<p>Monthly-ish design notes.</p>
<button type="button">Subscribe</button>
</div>
</div>
<div class="live-demo-variant" data-variant="1">
<div class="live-demo-card live-demo-card--v1">
<span class="live-demo-card-kicker">No. 04</span>
<h3>Letters, <em>occasionally</em>.</h3>
<p>A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
<button type="button">Send me one</button>
</div>
</div>
<div class="live-demo-variant" data-variant="2">
<div class="live-demo-card live-demo-card--v2">
<div class="live-demo-card-stamp"></div>
<span class="live-demo-card-kicker">Dispatch</span>
<h3>Design&nbsp;notes, <br>every&nbsp;other<br>Thursday.</h3>
<button type="button">Join the list </button>
</div>
</div>
<div class="live-demo-variant" data-variant="3">
<div class="live-demo-card live-demo-card--v3">
<div class="live-demo-card-sticker"><span>&star;</span><span>&star;</span><span>&star;</span></div>
<span class="live-demo-card-kicker">Field Notes</span>
<h3>A monthly letter, for people who still read email for pleasure.</h3>
<button type="button">Receive the letter <span aria-hidden="true"></span></button>
</div>
</div>
</div>
<div class="live-demo-outline" data-demo-outline aria-hidden="true"></div>
<div class="live-demo-annotations" data-demo-annotations aria-hidden="true">
<svg class="live-demo-stroke" viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
<path d="M 10,40 Q 60,10 110,38 T 210,32 T 290,20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" pathLength="1"/>
</svg>
<div class="live-demo-comment">more playful</div>
</div>
<div class="live-demo-ctx" data-demo-ctx data-phase="hidden">
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
<button type="button" class="live-demo-ctx-pill" data-demo-ctx-pill>
<span data-demo-cmd-name>delight</span>
<span class="live-demo-ctx-pill-caret" aria-hidden="true"></span>
</button>
<span class="live-demo-ctx-input" data-demo-input>
<span data-demo-input-text></span><span class="live-demo-ctx-caret"></span>
</span>
<button type="button" class="live-demo-ctx-count">×3</button>
<button type="button" class="live-demo-ctx-go" data-demo-go>Go <span aria-hidden="true"></span></button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--generating">
<span class="live-demo-ctx-spinner" aria-hidden="true"></span>
<span>Generating variants</span>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--cycling">
<button type="button" class="live-demo-ctx-nav" aria-label="Previous variant"></button>
<span class="live-demo-ctx-counter" data-demo-counter>1 / 3</span>
<button type="button" class="live-demo-ctx-nav" aria-label="Next variant"></button>
<span class="live-demo-ctx-divider"></span>
<button type="button" class="live-demo-ctx-discard" aria-label="Discard"></button>
<button type="button" class="live-demo-ctx-accept" data-demo-accept>Accept</button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--accepted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span>Variant 3 written to source</span>
</div>
</div>
<div class="live-demo-cursor" data-demo-cursor aria-hidden="true">
<svg width="18" height="22" viewBox="0 0 18 22" fill="none">
<path d="M1 1 L1 17 L5 13 L8 20 L11 19 L7.5 12 L13 12 Z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="live-demo-gbar" data-demo-gbar>
<span class="live-demo-gbar-brand">/</span>
<button type="button" class="live-demo-gbar-btn is-active" data-demo-gbar-pick>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
<span>Pick</span>
</button>
<button type="button" class="live-demo-gbar-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button type="button" class="live-demo-gbar-btn">
<span class="live-demo-gbar-dmd" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
</button>
<span class="live-demo-gbar-divider"></span>
<button type="button" class="live-demo-gbar-x" aria-label="Exit live mode"></button>
</div>
</div>
</div>
</div>`;
}
/**
* Render the /live-mode page main content.
*
* Marketing-style single-column layout mirroring /visual-mode's structure.
* Surfaces the animated homepage live-demo, a three-stage narrative,
* pathway cards (tutorial / reference / install), and the supported
* framework strip.
*/
function renderLiveModeMain() {
return `
<div class="live-mode-page">
<header class="live-mode-page-header">
<p class="live-mode-page-eyebrow">New in v3.0 <span class="live-mode-page-eyebrow-badge">Beta</span></p>
<h1 class="live-mode-page-title">Live Mode</h1>
<p class="live-mode-page-lede">Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.</p>
<div class="live-mode-start" aria-label="Start live mode command">
<span class="live-mode-start-prompt">$</span>
<code class="live-mode-start-cmd">/impeccable live</code>
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
</button>
</div>
</header>
<section class="live-mode-demo-wrap" aria-label="Live Mode interactive demo">
${renderLiveModeDemo()}
<p class="live-mode-demo-caption">Click the frame or scroll it into view to start the loop. Respects <code>prefers-reduced-motion</code>.</p>
</section>
<section class="live-mode-stages" aria-label="What happens in a live mode session">
<h2 class="live-mode-stages-title">What happens, in three moves</h2>
<div class="live-mode-stages-grid">
<article class="live-mode-stage">
<span class="live-mode-stage-num">01 &middot; Pick</span>
<h3 class="live-mode-stage-name">Point at what bugs you</h3>
<p class="live-mode-stage-desc">Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-picker-row" style="min-height:72px;padding:10px">
<div class="docs-viz-picker-target" style="font-size:12px;padding:6px 12px">
Newsletter card
<span class="docs-viz-picker-pin" style="width:18px;height:18px;font-size:9px">1</span>
</div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">02 &middot; Generate</span>
<h3 class="live-mode-stage-name">Three genuinely different takes</h3>
<p class="live-mode-stage-desc">Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-variants" style="width:100%;gap:4px">
<div class="docs-viz-variant docs-viz-variant--v1" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">No.04</span></div>
<div class="docs-viz-variant docs-viz-variant--v2 is-active" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Dispatch</span></div>
<div class="docs-viz-variant docs-viz-variant--v3" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Field</span></div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">03 &middot; Accept</span>
<h3 class="live-mode-stage-name">Lands in real source</h3>
<p class="live-mode-stage-desc">The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.</p>
<div class="live-mode-stage-viz">
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
</div>
</article>
</div>
</section>
<section class="live-mode-pathways" aria-label="Where to go next">
<h2 class="live-mode-pathways-title">Where next</h2>
<div class="live-mode-pathways-grid">
<a class="live-mode-pathway" href="/tutorials/iterate-live">
<span class="live-mode-pathway-kind">Tutorial</span>
<h3 class="live-mode-pathway-title">Walk it step by step</h3>
<p class="live-mode-pathway-desc">A ten-minute walkthrough from first run to accepted variant. Covers CSP patching, the picker actions, and the fallback flow for generated files.</p>
<span class="live-mode-pathway-cta">Open the tutorial &rarr;</span>
</a>
<a class="live-mode-pathway" href="/docs/live">
<span class="live-mode-pathway-kind">Reference</span>
<h3 class="live-mode-pathway-title">Full command reference</h3>
<p class="live-mode-pathway-desc">Everything your AI harness reads when <code>/impeccable live</code> runs: the poll loop, the wrap/accept helpers, the CSP templates, and every event shape.</p>
<span class="live-mode-pathway-cta">Read the reference &rarr;</span>
</a>
<a class="live-mode-pathway" href="/#downloads">
<span class="live-mode-pathway-kind">Install</span>
<h3 class="live-mode-pathway-title">Get Impeccable set up</h3>
<p class="live-mode-pathway-desc">Install the skill and CLI once, then run <code>/impeccable live</code> from your AI harness. Works with Claude Code, Cursor, Codex, Gemini, and more.</p>
<span class="live-mode-pathway-cta">See the install steps &rarr;</span>
</a>
</div>
</section>
<section class="live-mode-frameworks" aria-label="Supported frameworks">
<span class="live-mode-frameworks-label">Supported dev servers</span>
<ul class="live-mode-frameworks-list">
<li>Vite</li>
<li>Next.js (incl. monorepos)</li>
<li>SvelteKit</li>
<li>Astro</li>
<li>Nuxt</li>
<li>Bun</li>
<li>Plain static HTML</li>
</ul>
</section>
</div>`;
}
/**
* Render a tutorial detail page main content.
*/
@@ -647,7 +890,7 @@ ${rules.map(renderRuleCard).join('\n')}
<header class="anti-patterns-header">
<p class="sub-page-eyebrow">${totalRules} rules</p>
<h1 class="sub-page-title">Anti-patterns</h1>
<p class="sub-page-lede">The full catalog of patterns <a href="/docs/impeccable">/impeccable</a> teaches against. ${detectedCount} are caught by a deterministic detector (<code>npx impeccable detect</code> or the browser extension). ${llmCount} can only be flagged by <a href="/docs/critique">/impeccable critique</a>'s LLM review pass. Want to see them live on real pages? Try <a href="/visual-mode">Visual Mode</a>, or iterate past them on your own dev server with <a href="/docs/live">Live Mode</a>.</p>
<p class="sub-page-lede">The full catalog of patterns <a href="/docs/impeccable">/impeccable</a> teaches against. ${detectedCount} are caught by a deterministic detector (<code>npx impeccable detect</code> or the browser extension). ${llmCount} can only be flagged by <a href="/docs/critique">/impeccable critique</a>'s LLM review pass. Want to see them live on real pages? Try <a href="/visual-mode">Visual Mode</a>, or iterate past them on your own dev server with <a href="/live-mode">Live Mode</a>.</p>
</header>
<details class="anti-patterns-legend">
@@ -684,6 +927,7 @@ export async function generateSubPages(rootDir) {
antiPatterns: path.join(rootDir, 'public/anti-patterns'),
tutorials: path.join(rootDir, 'public/tutorials'),
visualMode: path.join(rootDir, 'public/visual-mode'),
liveMode: path.join(rootDir, 'public/live-mode'),
};
// Fresh output dirs each time so stale files don't linger.
@@ -784,6 +1028,31 @@ export async function generateSubPages(rootDir) {
generated.push(out);
}
// Live Mode: marketing landing mirroring /visual-mode. Needs live-mode.css
// (not imported by sub-pages.css to keep the base bundle small) and the
// live-demo JS module to animate the demo.
{
const extraHead = `
<link rel="stylesheet" href="../css/live-mode.css">
<script type="module">
import { initLiveDemo } from "../js/components/live-demo.js";
document.addEventListener("DOMContentLoaded", initLiveDemo);
</script>`;
const html = renderPage({
title: 'Live Mode | Impeccable',
description:
'Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live.',
bodyHtml: renderLiveModeMain(),
activeNav: 'live',
canonicalPath: '/live-mode',
bodyClass: 'sub-page live-mode-page-body',
extraHead,
});
const out = path.join(outDirs.liveMode, 'index.html');
fs.writeFileSync(out, html, 'utf-8');
generated.push(out);
}
// Tutorial detail pages.
for (const tutorial of data.tutorials) {
const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'tutorial', slug: tutorial.slug });