Compare commits

...
Author SHA1 Message Date
Paul BakausandClaude Opus 4.8 b913668ba4 Remove the i- command prefix from the CLI
The `i-` prefix install option was a holdover from the multi-skill era.
With a single `impeccable` skill it only ever renamed that one skill to
`i-impeccable`, while the install message wrongly advertised `/i-audit`
style commands that never existed, and the unscoped rename could clobber
unrelated third-party skills in the same harness folder.

- Drop `--prefix=`, the interactive prompt, and all prefix machinery
  (renameSkillsWithPrefix, prefixSkillContent, detectPrefix, undoPrefix,
  prefixedCommandHint, isImpeccableSkillName).
- Add migrateUnprefixImpeccable: install --force and update rename any old
  `<prefix>impeccable` back to canonical `impeccable` before the fresh copy
  lands, scoped by name so foreign `i-*` skills are left untouched.
- Fix FAQ + editorial that wrongly described pinned commands as `i-`
  prefixed (pins are bare `skills/<command>/` dirs).
- Tests now exercise the real exported migration, not a reimplementation.
- CLI 2.3.1 -> 2.3.2 with a changelog entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 18:33:16 -07:00
Abdul WahabandGitHub e10cff397b Fix live copy edit paragraph resizing (#178) 2026-05-29 13:03:28 -07:00
Paul BakausandClaude Opus 4.8 0c05cb8d2b Lighten the IMPECCABLE wordmark from weight 500 to 400
The lockup read a touch heavy. Drop the brand wordmark to 400 across the
header, footer, and the .ks-wordmark kit primitive so it stays consistent
everywhere. Alumni Sans was only loaded at 500/600/700, so 400 is added to
the font request (otherwise it would snap back to 500). DESIGN.md synced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:08:08 -07:00
Paul BakausandClaude Opus 4.8 4e985f68c0 Rework Get started section: add update instructions, fix layout rhythm
Closes the documentation gap from #177: how to update an installed
version was nowhere on the site. Install and Update now sit as paired,
equally-visible commands, with `npx impeccable skills check` and the
Claude Code `/plugin` path called out alongside.

Also a full pass on the section's composition:
- Commit to left-aligned asymmetry so content has one spine and the gold
  seam owns the right edge, instead of floating left-of-center
- Make the install command pop: bright kinpaku frame + gold `$` prompt +
  left-aligned mono so it reads as a runnable line, not a decorative chip.
  Update box mirrors it one notch quieter in patina (the "updated" state)
- Group install/update/alternatives tightly, rule off the secondary
  surfaces, drop the duplicate "Get started:" closing label
- Repurpose the "Stay updated" cell to "Follow along" so it stops
  colliding with the real Update command

FAQ already had a strong #update entry; added the `skills check`
companion for parity. Getting-started tutorial gains a short update note
after Step 1. Both themes synced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:02:14 -07:00
c8e973b324 Site polish: de-warm the palette, refine chrome, clean up /designing (#176)
* fix(home): command-wheel contrast, slop copy, line-length

Address P1/P2/P3 findings from /impeccable critique of the homepage:

- Command wheel (The Language): off-center command names floored at
  ~1.43:1 contrast were illegible (WCAG 1.4.3 fail) and hid most of the
  23-command vocabulary. Raise the fisheye opacity floor 0.25 -> 0.62,
  MIN_SCALE 0.35 -> 0.52, and lift the base color from --ks-text-muted to
  --ks-text. Off-center now measures >=4.59:1; full list stays scannable
  while gold + size + weight still carry focus.
- Slop section copy: rewrite all 7 discipline cards off the uniform
  "No X. No Y. No Z." triad into varied cadence with positives, and lead
  the section with what Impeccable does instead of the "Skills can't..."
  negation pivot. Drops the en-dash joiners too.
- Line length: cap .downloads-rebuild-note (was unbounded, ~102ch) and
  tighten the homepage .section-lead 68ch -> 62ch (rendered ~86 actual
  chars/line).

The diagonal plinth ramp on the slop grid is intentionally kept per
design preference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(home): de-warm and brighten the text ramp

Warmth now lives only in the gold accents and surfaces, not the type.
Body, headings, and the secondary tiers read crisp on lacquer instead of
mushing into the warm floor:

- --ks-champagne 84% .035 82 -> 91% .006 90 (headings/strong, now neutral;
  token name kept for compatibility)
- --ks-text 81% .03 82 -> 88% .008 90 (body)
- --ks-text-muted / -faint / -mute-deep lifted and de-warmed to match

DESIGN.md frontmatter + prose synced to the new values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(home): outlined testimonials + cleaner hero boundary

Testimonials:
- Flatten the double container: drop the t-plinth shelf wrapper (markup +
  CSS); the marquee sits directly in the section and carries its inset.
- Outlined cards: no fill, 1px solid neutral border (oklch .64 0 0 / .22),
  no dead drop-shadow. Removes the mushy gray-on-gray blend and the stacked
  dotted-divider + gold-hairline chrome.
- Section has no background of its own (rides the body lacquer gradient) and
  no top padding, so cards sit right under the hero divider.

Hero:
- Drop the bottom fade and the top nav scrim; the kintsugi art runs at full
  strength. A 1px neutral border-bottom (matching the card border) marks the
  testimonials boundary instead of a wash.
- "How it works" is the kit ghost link (white), not an outlined button.

Foundation/slop cards: lift the surface 9% -> 15% so they read as raised
specimen cards instead of vanishing into the ground.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(live): quieter, more refined picker chrome

Gold is reserved for the brand mark and the active control instead of
ringing every container. Applied to the homepage demo, /live-mode, and the
real injected picker (skill/scripts/live-browser.js, rebuilt into the
harness dirs):

- Container: neutral 1px hairline + tight neutral shadow (was a 1.5px gold
  border + gold halo ring); radius 10px -> 8px.
- Active toggle: crisp graphite pill with gold text (was a murky kinpaku-dim
  wash).
- Internal control borders (action pill / input / count): neutral hairline
  (was a warm gold rule); configure-row controls share one 30px baseline.
- Pick outline: crisp 1.5px line, no soft gold glow ring; tighter radius.
- Demo browser chrome: small uniform neutral dots, neutral URL pill, slimmer
  bar; frame edge neutral hairline + tighter shadow that registers on dark.

DESIGN.md "Live Mode Picker" spec + "Picker Is Brand Rule" updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(design): neutral default hairline (--ks-rule)

The default border/divider token was a warm gold hairline, used ~200x as
the site-wide default border — so every small label, pill, counter, card,
and divider carried warmth. Redefine it neutral so borders read clean;
gold stays where it signals.

- --ks-rule oklch(58% 0.065 82 / 0.32) -> oklch(78% 0 0 / 0.16)
- --ks-rule-strong (active/focus/brand borders) unchanged, still gold
- GitHub star pill: explicit near-white border (oklch 92% 0 0 / 0.18)
- DESIGN.md hairline mirror + prose synced

Verified across home, /design-system, and /docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(home): testimonials separator + visible star-pill border

- Move the dotted accent to the bottom of the testimonials (neutral dots)
  as a deliberate separator into the slop section; drop the oversized
  bottom padding to 1em so cards sit near the separator.
- Star-counter pill: solid oklch(80% 0 0) border. The previous near-white
  at 0.32 alpha rendered as faded mid-gray on the near-black pill; a solid
  light border reads as the intended white hairline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(design): neutralize body text (--ks-text)

Drop the last bit of warm chroma from the body text token; it still read
slightly warm at 0.008 chroma.

--ks-text oklch(88% 0.008 90) -> oklch(88% 0 0)  (pure neutral)

DESIGN.md mirror + prose synced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(design): neutral text everywhere

Zero the residual warm chroma across the rest of the text ramp so no text
tier carries warmth (warmth lives only in gold accents + surfaces).

- --ks-champagne 91% .006 90 -> 91% 0 0
- --ks-text-muted 72% .01 90  -> 72% 0 0
- --ks-text-faint 62% .008 90 -> 62% 0 0
- --ks-text-mute-deep 52% .008 90 -> 52% 0 0

(--ks-text was already neutralized.) DESIGN.md mirror + prose synced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(home): crisp, neutral Desloppification section

- Foundation card background: oklch(15% .004 95) -> oklch(15% 0 0) (neutral
  graphite) so cards read crisp, not warm.
- Plinth hatch: kinpaku gold -> neutral (oklch 80% 0 0 / .07) on a neutral
  base; the gold hatch was washing the section champagne.
- Remove the plinth bottom mask-fade so the pedestals end on a clean edge.

Gold stays only on the card icons as the accent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(home): drop the homepage warm-token overrides

The homepage redefined --ks-rule, --ks-rule-strong, and --ks-muted to warm
values locally (an old "busier surfaces" tweak), so homepage borders and
secondary text stayed champagne even after the global de-warm. That's why
the Desloppification cards still read warm.

- Remove the --ks-rule / --ks-rule-strong overrides; inherit the global
  tokens (neutral default border, gold strong/active border).
- Alias --ks-muted to the global --ks-text-muted (no divergent value);
  legacy code still reads the --ks-muted name.

Result: all homepage borders + secondary text are neutral; gold stays on
accents (icons, mark, CTAs, active/focus).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(design): canonical code tokens (inline + block)

Code styling was all over the place: homepage inline code was gold-on-gold,
the slop CLI was a one-off gold-on-raised-lacquer, downloads used a separate
--card-cmd-* set, docs used yet another. Add one shared token set and point
the canonical surfaces at it.

New :root tokens:
- --ks-code-fg / --ks-code-bg / --ks-code-radius      (inline: neutral chip)
- --ks-code-block-fg / -bg / -border / -radius        (block/CLI: lacquer terminal)
- --ks-code-cmd                                        (code that's a command link)

Migrated: homepage inline code (was gold -> neutral chip), slop-teaser-cli
(the "weird color" -> neutral terminal), downloads-cmd, and the docs inline +
fenced-block rules (now the token source of truth; block text also neutralized).

Remaining pages (designing, changelog/faq, detector, case studies) swept next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(design): sweep remaining pages onto code tokens

Point the rest of the site's code rules at the shared code tokens so inline
code and blocks are consistent everywhere:

- Inline code (designing, changelog, faq): gold -> neutral chip
  (--ks-code-fg / --ks-code-bg).
- Detector rule pills + table cells: code text -> --ks-code-fg.
- Neon-mirai case-study code block -> --ks-code-block-* tokens.

Command tags (the gold /command pills: spread-flow-cmd, docs-flow-cmd,
designing-phase-cmd, why-ci-cmd, etc.) are intentionally left as their own
interactive category, not generic code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(home): polish "The Language" section

- Command tags (/polish, /adapt): gold command text on a neutral code chip
  (--ks-code-cmd / --ks-code-bg), dropping the muddy gold-tint border.
- Commands panel kept as the solid oklch(0.17 0 0) panel (no border).
- Demo preview cleaned up to a single framed split: strip the grid ::before,
  the gold-grid/radial-glow container background, and the inner drop-shadow;
  before-half inherits the panel, after-half is near-black, with one thin
  neutral border on the demo itself (caption sits outside it).
- Periodic table: crisp flat neutral graphite tiles. Removed the JS-inlined
  category bg (var(--cat-*-bg)) + 1.5px colored border + hover drop-shadow,
  the gold-leaf ::before texture, the ::after accent line, the inset box-shadow,
  and the gold hover glow. Now a 1px neutral border, white symbols, readable
  neutral names, and a clean neutral-border hover with no shadow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(design): roomier inline-code padding

Inline code chips were tight top/bottom (the homepage one was only 0.05em).
Add a --ks-code-pad token (0.3em 0.5em) and point every inline-code rule at
it so the chips have consistent breathing room.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(home): drop non-steering commands from the command palette

impeccable, init, extract, document, and live are setup/management commands,
not steering verbs. Filter them out of the palette (fisheye + mobile carousel)
via a shared PALETTE_EXCLUDED set. They stay in the periodic table, which is
rendered separately by framework-viz.js.

Palette: 23 -> 18 commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(designing): flatten the pre-ship nested box

The pre-ship cards were a box-in-box: a legacy .designing-polish-grid panel
(cream bg + L/R/B border + padding, from docs-visuals.css) wrapping cards that
already have their own border + fill. Override the grid to a plain transparent
layout so the three cards are the only surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(designing): remove step counter + flatten design-debt boxes

- Drop the cryptic "03 · 04" pre-ship step counter (.designing-polish-band-meta)
  and tighten the band to a single bottom hairline.
- Design-debt: flatten the box-in-box (bento plinth > tile > stage). The
  .designing-maintain-stage no longer adds its own border + fill; the demo
  sits directly in the bento tile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(designing): neutralize code/terminal/panel surfaces

The terminal block, surface-cmd chips, command pills, live-frame, and other
dark panels used a slightly-warm dark fill (oklch 1X% 0.006 95). Drop the warm
chroma so they read neutral like the rest of the de-warmed site; the page
ground + deep surfaces stay lacquer-warm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(designing): neutralize inline code + live-mock picker chrome

- Inline code: the phase-sub and avoid-title code were still gold; point them
  at --ks-code-fg so all inline code reads neutral (gold stays only on command
  *links*).
- docs-viz-live mock: bring the duplicated picker chrome in line with the
  refactored neutral treatment — neutral 1px container borders (no gold halo),
  neutral active "Pick" pill (was the kinpaku-dim wash), crisp pick outline
  (no glow), tighter radii.
- CTAs (SEND ME ONE, Accept): pale-cream kinpaku-pale -> solid kinpaku gold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(designing): flatten lanes + avoid sections

- Brand/Product lane mock cards: drop the inner border+fill box; the mock sits
  directly in the bento tile, separated by a top hairline (no plinth>tile>mock
  nesting).
- "What to avoid" list: flatten the boxed list cards into a clean divided list
  (hairline separators, no per-item border/fill).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(designing): clean up the Brand/Product lanes

- Drop the bento plinth (0.17 fill + 8px gutter that drew the weird gutter
  "borders") and the tile fill; the two lanes sit on the page split by a single
  center hairline.
- Brand mock title used the pinstripe display face at 1.6rem (reads broken at
  that size, the "champagne text"); switch it to the clean body face so it
  matches the product mock title.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(site): update GitHub star count to 31k

31,188 stars as of now; header pill + aria-label were stale at 30k.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add Neo Kinpaku light mode across the site.

Wire theme persistence and a header toggle, then layer light-mode overrides for docs viz contrast, command demos, live-mode pathway cards, and the designing/home surfaces.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(designing): replace em dashes flagged by prose validator

Brand/Product lane copy used em dashes ("the deliverable —", "the task —");
swap for colons per STYLE.md so the Cloudflare build's validateProse passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(designing): address Cursor bugbot nits

- Fold the duplicate .designing-avoid { gap: 0 } override into the original
  rule (the gap: 18px was dead code).
- Drop the leftover el.style.boxShadow = 'none' in the periodic-tile deactivate
  handler — activate no longer sets a box-shadow, so this only left a dead
  inline none that could suppress a future CSS shadow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 03:50:23 -07:00
Paul BakausandClaude Opus 4.8 63074dd362 docs(changelog): drop stale Codex sidecar bullet from v3.5.0
The .codex/agents sidecar + boot-time self-heal it described was reverted
in CLI v2.3.1 (nested in-skill agent is the whole delivery now), so the
bullet no longer matched shipped behavior. Removed from the changelog and
the skill-v3.5.0 GitHub release notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:30:46 -07:00
47 changed files with 3014 additions and 1186 deletions
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
+24 -14
View File
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
+24 -14
View File
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
+24 -14
View File
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
+21 -21
View File
@@ -18,18 +18,18 @@ colors:
graphite-2: "oklch(19% 0.008 95)" # one step up from graphite
# Text
champagne: "oklch(84% 0.035 82)" # headlines, <strong>
text-warm: "oklch(81% 0.03 82)" # body
text-muted: "oklch(63% 0.024 82)" # captions, meta
text-faint: "oklch(52% 0.018 82)" # subdued
text-mute-deep: "oklch(48% 0.018 82)" # disabled
champagne: "oklch(91% 0 0)" # headlines, <strong> — neutral white (name kept for compat)
text-warm: "oklch(88% 0 0)" # body — neutral near-white
text-muted: "oklch(72% 0 0)" # captions, meta
text-faint: "oklch(62% 0 0)" # subdued
text-mute-deep: "oklch(52% 0 0)" # disabled
# Gold ramp
kinpaku-pale: "oklch(86% 0.07 84)" # hover lift, pale fills
kinpaku-rich: "oklch(77% 0.13 82)" # active CTA, severity-medium
kinpaku-deep: "oklch(61% 0.085 78)" # borders against the brand
gold-hairline: "oklch(58% 0.065 82 / 0.32)" # default rule
gold-hairline-strong: "oklch(74% 0.09 82 / 0.6)" # active rule
gold-hairline: "oklch(78% 0 0 / 0.16)" # default rule — neutral (name kept for compat)
gold-hairline-strong: "oklch(74% 0.09 82 / 0.6)" # active rule — gold
# Patina ramp
patina-pale: "oklch(82% 0.07 188)" # hover lift on patina
@@ -45,7 +45,7 @@ typography:
# reads too thin in the small lockup, so the wordmark uses the solid cut.
fontFamily: "Alumni Sans, Alumni Sans Pinstripe, Albert Sans, Arial, sans-serif"
fontSize: "1.3rem"
fontWeight: 500
fontWeight: 400
letterSpacing: "0.15em"
lineHeight: 1
display:
@@ -183,7 +183,7 @@ Every class below is a global primitive. Drop it on any element on any page usin
- `.ks-brand` — wrapper for the brand mark + wordmark lockup (anchor or div).
- `.ks-mark` — the carved-tile glyph (a solid kinpaku square split by a diagonal slash), 38×38, no container border.
- `.ks-wordmark` — the IMPECCABLE wordmark text, solid Alumni Sans (`--ks-font-wordmark`), uppercase, weight 500, letter-spacing 0.15em.
- `.ks-wordmark` — the IMPECCABLE wordmark text, solid Alumni Sans (`--ks-font-wordmark`), uppercase, weight 400, letter-spacing 0.15em.
**Section scaffolding**
@@ -272,16 +272,16 @@ Do not hand-type oklch values or font sizes in page CSS. If a value isn't in the
- **Kinpaku Rich** (`oklch(77% 0.13 82)`): Active CTA fill and severity-medium markers.
- **Kinpaku Deep** (`oklch(61% 0.085 78)`): Secondary gold for borders, subdued icons, and large technical diagrams.
- **Kinpaku Pale** (`oklch(86% 0.07 84)`): Hover lift and pale fills.
- **Gold Hairline** (`oklch(58% 0.065 82 / 0.32)`): Default border and divider. The homepage's busier surfaces lift this to alpha 0.48 locally.
- **Strong Gold Hairline** (`oklch(74% 0.09 82 / 0.6)`): Active borders, focus outlines, and structural anchors.
- **Default Hairline** (`oklch(78% 0 0 / 0.16)`): Default border and divider. Neutral, so borders and labels don't carry warmth (token name `gold-hairline` / `--ks-rule` is legacy).
- **Strong Gold Hairline** (`oklch(74% 0.09 82 / 0.6)`): Active borders, focus outlines, and structural anchors. Stays gold — this is where the hairline is meant to read as brand.
### Text
- **Champagne** (`oklch(84% 0.035 82)`): Headlines, `<strong>`, important labels.
- **Warm Text** (`oklch(81% 0.03 82)`): Body copy on dark surfaces.
- **Muted Text** (`oklch(63% 0.024 82)`): Metadata, captions, secondary labels. The homepage lifts this to 65% on busier tile backgrounds.
- **Faint Text** (`oklch(52% 0.018 82)`): Subdued labels.
- **Mute Deep** (`oklch(48% 0.018 82)`): Disabled copy.
- **Champagne** (`oklch(91% 0 0)`): Headlines, `<strong>`, important labels. The brightest text tier, fully neutral (token name is legacy). Text carries no warmth at any tier; the gold accents and surfaces do.
- **Body Text** (`oklch(88% 0 0)`): Body copy on dark surfaces. Neutral and bright so reading copy reads crisp, not mushy.
- **Muted Text** (`oklch(72% 0 0)`): Metadata, captions, secondary labels.
- **Faint Text** (`oklch(62% 0 0)`): Subdued labels.
- **Mute Deep** (`oklch(52% 0 0)`): Disabled copy.
### Secondary and State
@@ -310,7 +310,7 @@ The voice is geometric and restrained. The pinstripe display face is reserved fo
### Hierarchy
- **Wordmark**: solid Alumni Sans (`--ks-font-wordmark`), weight 500, uppercase, `1.3rem`, letter-spacing `0.15em`. Brand lockup only. The pinstripe sibling reads too thin at lockup size, so the wordmark uses the weightable cut.
- **Wordmark**: solid Alumni Sans (`--ks-font-wordmark`), weight 400, uppercase, `1.3rem`, letter-spacing `0.15em`. Brand lockup only. The pinstripe sibling reads too thin at lockup size, so the wordmark uses the weightable cut.
- **Display · h1**: Alumni Sans Pinstripe, `clamp(3.4rem, 6.5vw, 5.6rem)`, **weight 300**, line-height 1.02, letter-spacing `-0.01em`. Hero and major statements.
- **Headline · h2**: Alumni Sans Pinstripe, `clamp(2.6rem, 4vw, 3.4rem)`, **weight 600**, line-height 1.04. Section titles.
- **Title · h3**: Albert Sans, `1.18rem`, weight 500, line-height 1.35. Component and panel headings.
@@ -387,16 +387,16 @@ The footer can carry the strongest oxidation accent. Use the gold seam plus pati
The global bottom bar and the contextual bar (configure / cycling / accept) share one chrome treatment. Source of truth: `skill/scripts/live-browser.js` (`barPaletteForTheme`, `initGlobalBar`, `initBar`). Homepage and `/live-mode` demos mirror it via `.live-demo-gbar` and `.live-demo-ctx` in `site/styles/kinpaku-kit.css`.
- **Surface:** Lacquer Deep (`oklch(4% 0.004 95)`), always. Picker chrome does not adapt to the host page's light/dark theme.
- **Border:** 1.5px solid Kinpaku Gold (`oklch(84% 0.19 80.46)`).
- **Shadow:** `0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)` (kinpaku halo + drop shadow).
- **Border:** 1px solid neutral hairline (`oklch(92% 0 0 / 0.13)`), radius 8px. The bar reads as a quiet precise tool; gold is reserved for the brand mark and the active control, not the container outline.
- **Shadow:** `0 16px 36px -12px oklch(0% 0 0 / 0.6)` (tight neutral drop, no gold halo ring).
- **Brand mark:** Impeccable carved-tile icon (same SVG paths as `site/components/Header.astro` / `favicon.svg`), kinpaku fill on transparent ground. Not a "/" slash or rounded-square placeholder.
- **Default controls:** Champagne labels at rest (`oklch(84% 0.035 82)`), muted icons (`oklch(63% 0.024 82)`).
- **Active toggle:** Kinpaku-dim pill background (`oklch(78% 0.12 82 / 0.18)`) with kinpaku text.
- **Active toggle:** Crisp graphite pill (`oklch(27% 0 0)`) with kinpaku text/icon. The gold carries the "selected" signal; the pill itself is neutral, not a kinpaku-dim wash.
- **Exit hover:** Vermilion (`oklch(58% 0.15 35)`), not a neutral gray lift.
- **Context bar internals:** Graphite-2 pills, gold hairline dividers, kinpaku Go/Accept CTAs with lacquer-deep text.
- **DESIGN.md toggle icon:** Four-quadrant swatch; bottom-right uses warm charcoal (`oklch(34% 0.014 82)`) so the tile reads against lacquer-deep, not void-black.
**The Picker Is Brand Rule.** Live mode UI is Impeccable product chrome, not host-page chrome. It always ships the full kinpaku border, lacquer-deep fill, and carved-tile mark.
**The Picker Is Brand Rule.** Live mode UI is Impeccable product chrome, not host-page chrome. It always ships the lacquer-deep fill, the carved-tile mark, and gold on the mark + active control. Brand reads through the mark and the gold accent rather than a gold border ringing the bar; the container itself stays a quiet neutral-edged tool.
## 7. Do and Do Not
+46 -173
View File
@@ -8,7 +8,7 @@
*/
import { execSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync, lstatSync, symlinkSync, readlinkSync, unlinkSync, mkdirSync, writeFileSync, rmSync, renameSync, createWriteStream, realpathSync } from 'node:fs';
import { existsSync, readFileSync, readdirSync, statSync, lstatSync, unlinkSync, mkdirSync, writeFileSync, rmSync, renameSync, createWriteStream, realpathSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path';
import { createInterface } from 'node:readline';
import { fileURLToPath } from 'node:url';
@@ -237,31 +237,6 @@ function isAlreadyInstalled(root) {
return null;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function prefixSkillContent(content, prefix, allSkillNames) {
// Prefix the name in frontmatter
let result = content.replace(/^name:\s*(.+)$/m, (_, name) => `name: ${prefix}${name.trim()}`);
// Prefix cross-references: /skillname -> /prefix-skillname
const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
for (const name of sorted) {
// Command invocations: /skillname
result = result.replace(
new RegExp(`/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
`/${prefix}`
);
// Prose references: "the skillname skill"
result = result.replace(
new RegExp(`(the) ${escapeRegex(name)} skill`, 'gi'),
(_, article) => `${article} ${prefix}${name} skill`
);
}
return result;
}
function isSkillDir(skillsDir, name) {
// Skill entries can be real directories or symlinks to directories (npx skills uses symlinks)
const full = join(skillsDir, name);
@@ -279,63 +254,40 @@ function isRealSkillDir(skillsDir, name) {
} catch { return false; }
}
function renameSkillsWithPrefix(root, prefix) {
// First pass: collect all skill names across all providers (use first provider found)
let allSkillNames = [];
/**
* One-way migration for installs from the era when the CLI offered a command
* prefix (default `i-`), renaming the skill to e.g. `i-impeccable`. The prefix
* only earned its keep when every command was its own skill; with a single
* `impeccable` skill it does nothing, so it is no longer offered. Rename any
* prefixed impeccable skill back to the canonical `impeccable` (the fresh
* install/update content lands there next) so users aren't left with a stale,
* orphaned `i-impeccable` alongside the new one. Scoped to the impeccable skill
* by name -- never touches third-party skills that happen to start with `i-`.
* Returns the number of skills migrated.
*/
function migrateUnprefixImpeccable(root) {
let migrated = 0;
for (const d of PROVIDER_DIRS) {
const skillsDir = join(root, d, 'skills');
if (!existsSync(skillsDir)) continue;
const entries = readdirSync(skillsDir);
allSkillNames = entries.filter(name => isSkillDir(skillsDir, name));
if (allSkillNames.length > 0) break;
let entries;
try { entries = readdirSync(skillsDir); } catch { continue; }
for (const name of entries) {
// A prefixed impeccable skill is `<prefix>impeccable` -- not the canonical
// `impeccable`, and not the legacy `teach-impeccable` (cleanup handles that).
if (name === 'impeccable' || name === 'teach-impeccable') continue;
if (!name.endsWith('-impeccable')) continue;
if (!isRealSkillDir(skillsDir, name)) continue;
const dest = join(skillsDir, 'impeccable');
try {
rmSync(dest, { recursive: true, force: true });
renameSync(join(skillsDir, name), dest);
migrated++;
} catch {}
}
}
// Second pass: rename real dirs and update their content
let count = 0;
for (const d of PROVIDER_DIRS) {
const skillsDir = join(root, d, 'skills');
if (!existsSync(skillsDir)) continue;
try {
const entries = readdirSync(skillsDir);
for (const name of entries) {
if (name.startsWith(prefix)) continue;
if (!isRealSkillDir(skillsDir, name)) continue;
const src = join(skillsDir, name);
const dest = join(skillsDir, prefix + name);
renameSync(src, dest);
// Prefix frontmatter name + all cross-references in SKILL.md
let content = readFileSync(join(dest, 'SKILL.md'), 'utf8');
content = prefixSkillContent(content, prefix, allSkillNames);
writeFileSync(join(dest, 'SKILL.md'), content);
count++;
}
} catch {}
}
// Third pass: fix symlinks that now point to renamed targets (npx skills uses these)
for (const d of PROVIDER_DIRS) {
const skillsDir = join(root, d, 'skills');
if (!existsSync(skillsDir)) continue;
try {
const entries = readdirSync(skillsDir);
for (const name of entries) {
if (name.startsWith(prefix)) continue;
const full = join(skillsDir, name);
try {
if (!lstatSync(full).isSymbolicLink()) continue;
const target = readlinkSync(full);
const newTarget = target.replace(new RegExp(`/${escapeRegex(name)}$`), `/${prefix}${name}`);
unlinkSync(full);
symlinkSync(newTarget, join(skillsDir, prefix + name));
} catch {}
}
} catch {}
}
return count;
return migrated;
}
/**
@@ -401,7 +353,6 @@ function copyProviderSkills(bundleDir, root, targets) {
async function install(flags) {
const force = flags.includes('--force');
const yes = flags.includes('-y') || flags.includes('--yes');
const prefixFlag = flags.find(f => f.startsWith('--prefix='));
const providersFlag = flags.find(f => f.startsWith('--providers='));
const root = findProjectRoot();
const existing = isAlreadyInstalled(root);
@@ -442,6 +393,10 @@ async function install(flags) {
process.exit(1);
}
// Retire any old `i-`-prefixed install so the fresh copy lands on the
// canonical `impeccable` dir instead of orphaning the prefixed one.
migrateUnprefixImpeccable(root);
let written = 0;
try {
written = copyProviderSkills(bundleDir, root, targets);
@@ -458,27 +413,6 @@ async function install(flags) {
}
console.log(`Installed impeccable into: ${targets.join(', ')}`);
// Ask about prefixing (skip in CI mode unless --prefix= is set)
let prefix = '';
if (prefixFlag) {
prefix = prefixFlag.split('=')[1] || 'i-';
} else if (!yes) {
console.log();
const wantPrefix = await ask('Prefix commands to avoid conflicts? e.g. /i-audit instead of /audit (y/N) ');
if (wantPrefix === 'y' || wantPrefix === 'yes') {
const custom = await ask('Prefix (default: i-): ');
prefix = custom || 'i-';
}
}
if (prefix) {
const count = renameSkillsWithPrefix(root, prefix);
if (count > 0) {
console.log(`\nRenamed ${count} skills with "${prefix}" prefix.`);
console.log(`Commands are now available as /${prefix}<command> (e.g. /${prefix}audit).`);
}
}
// Clean up deprecated skills from previous versions
try {
const { cleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs');
@@ -491,71 +425,7 @@ async function install(flags) {
// Cleanup script not available -- skip
}
console.log(`\nDone! Run /${prefix}impeccable init in your AI harness to set up design context.\n`);
}
/** Detect prefix by looking for the 'impeccable' skill (or legacy 'teach-impeccable') */
function detectPrefix(root) {
for (const d of PROVIDER_DIRS) {
const skillsDir = join(root, d, 'skills');
if (!existsSync(skillsDir)) continue;
for (const name of readdirSync(skillsDir)) {
if (name === 'impeccable') return '';
if (name.endsWith('-impeccable') && name !== 'teach-impeccable') return name.slice(0, -'impeccable'.length);
// Legacy fallback
if (name === 'teach-impeccable') return '';
if (name.endsWith('-teach-impeccable')) return name.slice(0, -'teach-impeccable'.length);
}
}
return '';
}
/** Undo prefixing: rename folders back and strip prefix from SKILL.md content */
function undoPrefix(root, prefix) {
if (!prefix) return;
// Collect the unprefixed names (strip our prefix)
let allPrefixedNames = [];
for (const d of PROVIDER_DIRS) {
const skillsDir = join(root, d, 'skills');
if (!existsSync(skillsDir)) continue;
allPrefixedNames = readdirSync(skillsDir).filter(n => n.startsWith(prefix) && isRealSkillDir(skillsDir, n));
if (allPrefixedNames.length > 0) break;
}
const unprefixedNames = allPrefixedNames.map(n => n.slice(prefix.length));
for (const d of PROVIDER_DIRS) {
const skillsDir = join(root, d, 'skills');
if (!existsSync(skillsDir)) continue;
for (const name of readdirSync(skillsDir)) {
if (!name.startsWith(prefix)) continue;
const unprefixed = name.slice(prefix.length);
const src = join(skillsDir, name);
const dest = join(skillsDir, unprefixed);
if (lstatSync(src).isSymbolicLink()) {
const target = readlinkSync(src);
const newTarget = target.replace(`/${name}`, `/${unprefixed}`);
unlinkSync(src);
symlinkSync(newTarget, dest);
} else {
renameSync(src, dest);
// Strip prefix from SKILL.md content
const skillMd = join(dest, 'SKILL.md');
if (existsSync(skillMd)) {
let content = readFileSync(skillMd, 'utf8');
// Reverse the prefixing: replace prefixed names with unprefixed
content = content.replace(new RegExp(`^name:\\s*${escapeRegex(prefix)}`, 'm'), 'name: ');
const sorted = [...allPrefixedNames].sort((a, b) => b.length - a.length);
for (const pName of sorted) {
const uName = pName.slice(prefix.length);
content = content.replace(new RegExp(`/${escapeRegex(pName)}(?=[^a-zA-Z0-9_-]|$)`, 'g'), `/${uName}`);
content = content.replace(new RegExp(`(the) ${escapeRegex(pName)} skill`, 'gi'), `$1 ${uName} skill`);
}
writeFileSync(skillMd, content);
}
}
}
}
console.log('\nDone! Run /impeccable init in your AI harness to set up design context.\n');
}
// ─── skills update ────────────────────────────────────────────────────────────
@@ -684,6 +554,11 @@ async function update(flags = []) {
try {
// Retire any old `i-`-prefixed install up front so the refresh lands on the
// canonical `impeccable` dir rather than orphaning the prefixed copy.
const migrated = migrateUnprefixImpeccable(root);
if (migrated > 0) console.log('Migrated a prefixed install back to /impeccable (the i- prefix is no longer used).');
// Copy from the bundle to each unique provider folder.
// Deduplicate so symlinked dirs (e.g. .claude/skills -> .agents/skills)
// are only written once with the correct provider's content.
@@ -706,13 +581,6 @@ async function update(flags = []) {
rmSync(tmpDir, { recursive: true, force: true });
// Re-apply prefix if detected
const prefix = detectPrefix(root);
if (prefix) {
const count = renameSkillsWithPrefix(root, prefix);
if (count > 0) console.log(`Re-applied "${prefix}" prefix to ${count} skills.`);
}
// Run cleanup to remove deprecated stubs from the fresh download
try {
const { cleanup: postCleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs');
@@ -744,6 +612,11 @@ function copyDirSync(src, dest) {
}
}
// ─── Test surface ───────────────────────────────────────────────────────────
// Exported so the test suite exercises the real implementation rather than a
// reimplementation in a helper script (which is how bugs slip through).
export { migrateUnprefixImpeccable };
// ─── Router ───────────────────────────────────────────────────────────────────
export async function run(args) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impeccable",
"version": "2.3.1",
"version": "2.3.2",
"author": "Paul Bakaus",
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
"keywords": [
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
+18 -2
View File
@@ -34,9 +34,25 @@ const { activeNav } = Astro.props;
<a href="/slop" data-nav="slop" aria-current={activeNav === 'slop' ? 'page' : undefined}>Slop</a>
<a href="/live-mode" data-nav="live" aria-current={activeNav === 'live' ? 'page' : undefined}>Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 30k stars">
<button
type="button"
class="theme-toggle"
data-theme-toggle
aria-label="Switch to light mode"
title="Light mode"
data-next-theme="light"
>
<svg class="theme-toggle-icon--light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<circle cx="12" cy="12" r="4.5"/>
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg class="theme-toggle-icon--dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3a7 7 0 1 0 11.5 11.5z"/>
</svg>
</button>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 31k 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">30k</span>
<span class="site-header-github-label">31k</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>
-2
View File
@@ -19,7 +19,6 @@ const rowB = all.slice(half);
---
<section class="testimonials-section" id="testimonials" aria-label="What people say about Impeccable">
<div class="t-plinth t-plinth--shelf">
<div class="t-marquee" aria-hidden="false">
<div class="t-marquee-row">
@@ -71,5 +70,4 @@ const rowB = all.slice(half);
</div>
</div>
</div>
</section>
+1 -1
View File
@@ -75,7 +75,7 @@ Useful pins to try:
- `/impeccable pin live` for the browser iteration flow
- `/impeccable pin critique` for design review
To remove: `/impeccable unpin critique`. Pins live as directories prefixed with `i-` in your harness skills folder (`.claude/skills/i-critique/`, `.cursor/skills/i-critique/`, etc.), so you can also delete them manually.
To remove: `/impeccable unpin critique`. Pins live as directories named after the command in your harness skills folder (`.claude/skills/critique/`, `.cursor/skills/critique/`, etc.), so you can also delete them manually.
## Pitfalls
@@ -38,6 +38,8 @@ This auto-detects your harness and writes the build compiled for it to the right
Prefer a different setup? Claude Code users can install the plugin with `/plugin marketplace add pbakaus/impeccable`, and the general-purpose `npx skills add pbakaus/impeccable` still works (though it installs one shared build for all harnesses rather than the one compiled for yours).
When a new version ships later, run `npx impeccable skills update` from the same project root. `npx impeccable skills check` tells you first whether you are behind, and plugin users update from the `/plugin` menu instead.
## Step 2. Set up Impeccable for your project
This is the most important step. Design without context produces generic output. The `/impeccable init` command runs a short discovery interview and writes a `PRODUCT.md` file at the root of your project.
+21 -1
View File
@@ -6,6 +6,7 @@ import Footer from '../components/Footer.astro';
// to re-import from page-level stylesheets.
import '../styles/kinpaku-tokens.css';
import '../styles/kinpaku-kit.css';
import '../styles/light-mode.css';
import '../styles/footer.css';
interface Props {
@@ -63,6 +64,21 @@ const resolvedOgDescription = ogDescription || description;
<title>{title}</title>
<meta name="description" content={description}>
<meta name="theme-color" content="#010101">
<script is:inline>
(function () {
try {
var t = localStorage.getItem('impeccable-theme');
var root = document.documentElement;
if (t === 'light') {
root.classList.add('light');
var meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute('content', '#f7f4ef');
} else {
root.classList.add('dark');
}
} catch (e) {}
})();
</script>
{noIndex && <meta name="robots" content="noindex">}
{canonical && <link rel="canonical" href={canonical}>}
@@ -84,7 +100,7 @@ const resolvedOgDescription = ogDescription || description;
<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=Albert+Sans:wght@300;400;500;600;700&family=Alumni+Sans:wght@500;600;700&family=Alumni+Sans+Pinstripe&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Albert+Sans:wght@300;400;500;600;700&family=Alumni+Sans:wght@400;500;600;700&family=Alumni+Sans+Pinstripe&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<slot name="head" />
</head>
<body class={bodyClass}>
@@ -97,5 +113,9 @@ const resolvedOgDescription = ogDescription || description;
</main>
{!hideFooter && <Footer />}
<slot name="scripts" />
<script>
import { initThemeToggle } from '../scripts/utils/theme.js';
initThemeToggle();
</script>
</body>
</html>
+8 -1
View File
@@ -67,11 +67,18 @@ import '../styles/changelog-faq-kinpaku.css';
<li><strong>A faster detector with no jsdom.</strong> The HTML/CSS engine was rebuilt from the ground up on <code>htmlparser2</code> and a real CSS cascade resolver, replacing jsdom. On the same 160-file HTML corpus it runs about 20x faster under Node: 0.34s where the old jsdom engine took 6.8s, roughly 2 ms per file instead of 43 ms. Dependency-free and small enough to bundle straight into the skill and run inline, not just in the CLI and the extension.</li>
<li><strong>Detector: 14 new rules.</strong> <code>cream-palette</code>, <code>em-dash-overuse</code>, <code>marketing-buzzword</code>, <code>numbered-section-markers</code>, <code>aphoristic-cadence</code>, <code>theater-slop-phrase</code>, <code>oversized-h1</code>, <code>extreme-negative-tracking</code>, <code>gpt-thin-border-wide-shadow</code>, <code>repeating-stripes-gradient</code>, <code>image-hover-transform</code>, <code>broken-image</code>, <code>text-overflow</code>, and <code>clipped-overflow-container</code>. 41 deterministic rules total, one canonical registry feeding the CLI, the browser extension, critique, and the evals.</li>
<li><strong>The skill keeps itself current.</strong> On the first session of the day, Impeccable quietly checks whether a newer version shipped. If one has, it offers to run <code>npx impeccable skills update</code> for you. It always asks first, never nags about a version you declined, and never interrupts the task you're on. Set <code>IMPECCABLE_NO_UPDATE_CHECK=1</code> to turn it off.</li>
<li><strong>Codex gets its asset-producer subagent automatically.</strong> Codex reads custom subagents from <code>.codex/agents/</code>, a directory separate from where it reads skills, so installers used to leave the agent behind. Now the skill bundles the agent, notices on boot when a Codex project is missing it, and offers to copy it into place. Fixes the gap reported in #161.</li>
<li><strong>Sharper craft under the hood.</strong> Beyond the bans, the craft itself got tighter. Small defaults landed where they pay off, like <code>text-wrap: balance</code> on headings, which cleaned up ragged hero type across the ablation runs. And the instructions themselves got leaner: orphan reference files folded into their commands, context loading simplified, and a new LLM-backed test suite that catches instruction-following regressions across three providers on every change. Plus dedicated <a href="/changelog">/changelog</a> and <a href="/faq">/faq</a> pages.</li>
</ul>
</article>
<article id="cli-v2.3.2" class="cf-entry">
<header class="cf-entry-head"><span class="cf-version">CLI v2.3.2</span><span class="cf-date">May 29, 2026</span></header>
<ul class="cf-items">
<li><strong>The <code>i-</code> command prefix is gone.</strong> Opting into a prefix at install time was a holdover from when every command was its own skill. With a single <code>impeccable</code> skill it only ever renamed that one skill to <code>i-impeccable</code>, while the install message wrongly promised <code>/i-audit</code> style commands that never existed, and the rename could clobber unrelated third-party skills in the same harness folder. The flag and the prompt are removed.</li>
<li><strong>Existing prefixed installs heal themselves.</strong> <code>skills install</code> and <code>skills update</code> now rename any old <code>i-impeccable</code> (or custom-prefixed) skill back to the canonical <code>impeccable</code>, scoped by name so a third-party skill that happens to start with <code>i-</code> is left untouched. Want a short top-level command? <code>/impeccable pin audit</code> still makes <code>/audit</code> a standalone shortcut.</li>
</ul>
</article>
<article id="cli-v2.3.1" class="cf-entry">
<header class="cf-entry-head"><span class="cf-version">CLI v2.3.1</span><span class="cf-date">May 28, 2026</span></header>
<ul class="cf-items">
+16
View File
@@ -41,6 +41,22 @@ import '../../styles/design-system.css';
<a href="#components">Components</a>
<a href="#proof">Proof</a>
</nav>
<button
type="button"
class="theme-toggle"
data-theme-toggle
aria-label="Switch to light mode"
title="Light mode"
data-next-theme="light"
>
<svg class="theme-toggle-icon--light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<circle cx="12" cy="12" r="4.5"/>
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg class="theme-toggle-icon--dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3a7 7 0 1 0 11.5 11.5z"/>
</svg>
</button>
<a class="ks-button ks-button-primary" href="#actions">
Start audit
<span class="ks-button-arrow" aria-hidden="true">
+16 -6
View File
@@ -391,17 +391,27 @@ import '../../styles/designing-kinpaku.css';
<div class="designing-phase-body">
<div class="ks-bento designing-lanes">
<article class="ks-bento-tile ks-bento-tile--span-6">
<p class="designing-lane-rule">Design IS the product. Marketing, landing, editorial, long-form, portfolio.</p>
<p class="designing-lane-rule"><strong>Brand.</strong> Design is the deliverable: landing pages, campaigns, editorial, portfolios. Distinctive type, committed palette, image-led heroes. The impression is the product.</p>
<div class="designing-lane-mock designing-lane-mock--brand" aria-hidden="true">
<span class="designing-lane-mock-label">No. 04 &middot; Dispatch</span>
<span class="designing-lane-mock-title">Letters, occasionally.</span>
<span class="designing-lane-mock-label">Issue 04 · Dispatch</span>
<span class="designing-lane-mock-title"><span class="designing-lane-mock-accent">Shape</span> the story.</span>
</div>
</article>
<article class="ks-bento-tile ks-bento-tile--span-6">
<p class="designing-lane-rule">Design serves the task. App UI, admin, dashboards, tools.</p>
<p class="designing-lane-rule"><strong>Product.</strong> Design serves the task: app UI, admin, dashboards, tools. Fluent density, semantic states, repeatable components. Users are here to finish something.</p>
<div class="designing-lane-mock designing-lane-mock--product" aria-hidden="true">
<span class="designing-lane-mock-label">Newsletter</span>
<span class="designing-lane-mock-title">Subscribe to updates</span>
<div class="designing-lane-product-row">
<span class="designing-lane-product-k">Users</span>
<span class="designing-lane-product-v">12,482</span>
</div>
<div class="designing-lane-product-row">
<span class="designing-lane-product-k">Active now</span>
<span class="designing-lane-product-v">1,207</span>
</div>
<div class="designing-lane-product-row">
<span class="designing-lane-product-k">Conversion</span>
<span class="designing-lane-product-v">4.8%</span>
</div>
</div>
</article>
</div>
+2 -2
View File
@@ -29,7 +29,7 @@ import '../styles/changelog-faq-kinpaku.css';
<details id="update" class="cf-faq-item">
<summary class="cf-faq-question">How do I update to the latest version?</summary>
<div class="cf-faq-answer">
<p>Run <code>npx impeccable skills update</code> from your project root. It downloads the latest skills, cleans up deprecated files, and preserves any prefix you use.</p>
<p>Run <code>npx impeccable skills update</code> from your project root. It downloads the latest skills and cleans up deprecated files. Not sure you're behind? <code>npx impeccable skills check</code> compares what you have installed against the latest release first.</p>
<ul>
<li><strong>Reinstall:</strong> <code>npx impeccable skills install --force</code> installs fresh.</li>
<li><strong>Claude Code plugin:</strong> Open <code>/plugin</code> in Claude Code.</li>
@@ -51,7 +51,7 @@ import '../styles/changelog-faq-kinpaku.css';
<li><code>/impeccable pin audit</code> &rarr; <code>/audit</code> works again</li>
<li><code>/impeccable pin live</code> &rarr; <code>/live</code> works again</li>
</ul>
<p>To remove: <code>/impeccable unpin critique</code>. To see your current pins, check your harness skills directory (<code>.claude/skills/</code>, <code>.cursor/skills/</code>, etc.) for directories prefixed with <code>i-</code>.</p>
<p>To remove: <code>/impeccable unpin critique</code>. To see your current pins, check your harness skills directory (<code>.claude/skills/</code>, <code>.cursor/skills/</code>, etc.) for directories named after the command you pinned, like <code>.claude/skills/critique/</code>.</p>
</div>
</details>
+43 -36
View File
@@ -71,7 +71,7 @@ import '../styles/testimonials.css';
</svg>
</span>
</a>
<a href="/designing" class="ks-button ks-button-secondary">How it works</a>
<a href="/designing" class="ks-button ks-button-ghost">How it works</a>
</div>
<div class="hero-rebuild-logos" aria-label="AI coding harnesses Impeccable works with">
@@ -250,7 +250,7 @@ import '../styles/testimonials.css';
<div class="slop-teaser-head">
<h2 id="slop-teaser-title" class="slop-teaser-title">Desloppification</h2>
<p class="slop-teaser-body">
Skills can't make a model fundamentally better at design. What they can do is remove inherent bias: the AI slop tells, the bad defaults, across every design discipline. That's what Impeccable does.
Impeccable strips the AI slop tells and bad defaults out of every design discipline, from type to motion to copy. A skill can't make the model a better designer; it can clear the reflexes that make its output look generated.
</p>
<div class="slop-teaser-actions">
<a class="slop-teaser-cta-primary" href="/slop">
@@ -541,11 +541,13 @@ import '../styles/testimonials.css';
<span class="why-live-pick-note">more elegant<span class="why-live-pick-caret"></span></span>
</div>
</div>
<div class="why-live-gbar">
<span class="why-live-gbar-brand">/</span>
<span class="why-live-gbar-cmd">bolder</span>
<span class="why-live-gbar-count">&times;3</span>
<span class="why-live-gbar-go">Go &rarr;</span>
<div class="why-live-ctx live-demo-ctx" data-phase="configuring">
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
<span class="live-demo-ctx-pill">bolder<span class="live-demo-ctx-pill-caret" aria-hidden="true">&#x25BE;</span></span>
<span class="live-demo-ctx-input"><span class="live-demo-ctx-caret"></span></span>
<span class="live-demo-ctx-count">&times;3</span>
<span class="live-demo-ctx-go">Go <span aria-hidden="true">&rarr;</span></span>
</div>
</div>
</div>
</div>
@@ -587,17 +589,37 @@ import '../styles/testimonials.css';
</div>
<div class="downloads-rebuild-install">
<div class="downloads-rebuild-cmd">
<code>npx impeccable skills install</code>
<button class="downloads-rebuild-copy" type="button" aria-label="Copy install command" data-copy="npx impeccable skills install">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
<span>Copy</span>
</button>
<div class="downloads-rebuild-cmd-group">
<span class="downloads-rebuild-cmd-label">Install</span>
<div class="downloads-rebuild-cmd">
<span class="downloads-rebuild-prompt" aria-hidden="true">$</span>
<code>npx impeccable skills install</code>
<button class="downloads-rebuild-copy" type="button" aria-label="Copy install command" data-copy="npx impeccable skills install">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
<span>Copy</span>
</button>
</div>
</div>
<p class="downloads-rebuild-note">Installs the build compiled for your harness. Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and every other major AI coding harness.</p>
<div class="downloads-rebuild-cmd-group">
<span class="downloads-rebuild-cmd-label downloads-rebuild-cmd-label--update">Update</span>
<div class="downloads-rebuild-cmd downloads-rebuild-cmd--update">
<span class="downloads-rebuild-prompt" aria-hidden="true">$</span>
<code>npx impeccable skills update</code>
<button class="downloads-rebuild-copy" type="button" aria-label="Copy update command" data-copy="npx impeccable skills update">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
<span>Copy</span>
</button>
</div>
</div>
<p class="downloads-rebuild-note">Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and every other major AI coding harness. <code>install</code> sets up the build for your harness; <code>update</code> pulls the latest. Run <code>npx impeccable skills check</code> to see if you're behind. Installed the Claude Code plugin? Update it from the <code>/plugin</code> menu instead.</p>
<details class="downloads-rebuild-alts">
<summary class="downloads-rebuild-alts-summary">
@@ -652,29 +674,14 @@ import '../styles/testimonials.css';
<a href="https://www.npmjs.com/package/impeccable" target="_blank" rel="noopener">View on npm →</a>
</li>
<li>
<strong>Stay updated</strong>
<span>New commands, new anti-patterns, and the design thinking behind them. Subscribe to the Substack, or follow along on X.</span>
<a href="https://impeccablestyle.substack.com" target="_blank" rel="noopener" class="downloads-rebuild-link-primary">Subscribe on Substack →</a>
<strong>Follow along</strong>
<span>The design thinking behind new commands and anti-patterns, in long form. Read the newsletter, or catch the shorter notes on X.</span>
<a href="https://impeccablestyle.substack.com" target="_blank" rel="noopener" class="downloads-rebuild-link-primary">Read the newsletter →</a>
<a href="https://x.com/impeccable_ai" target="_blank" rel="noopener" class="downloads-rebuild-link-secondary">Follow @impeccable_ai on X →</a>
</li>
</ul>
<ol class="downloads-rebuild-examples">
<li>
<code><span class="downloads-rebuild-slash">/</span>impeccable redo this hero</code>
<span>Let the skill pick the approach. No command names to memorize.</span>
</li>
<li>
<code><span class="downloads-rebuild-slash">/</span>impeccable audit checkout</code>
<span>Reach for a command by name. Type <code>/impeccable</code> alone to see all 23.</span>
</li>
<li>
<code><span class="downloads-rebuild-slash">/</span>impeccable pin audit</code>
<span>Pin your favorites. <code>/audit</code> becomes a standalone shortcut.</span>
</li>
</ol>
<p class="downloads-rebuild-more">Go deeper: <a href="/designing">Designing with Impeccable</a> &middot; <a href="/docs">the command docs</a></p>
<p class="downloads-rebuild-more">Learn more: <a href="/designing">Designing with Impeccable</a> &middot; <a href="/docs">the command docs</a></p>
</div>
</section>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

+1 -6
View File
@@ -240,15 +240,13 @@ export class PeriodicTable {
el.style.cssText = `
width: 56px;
height: 64px;
background: ${colors.bg};
border: 1.5px solid ${colors.border};
border-radius: 5px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
transition: transform 0.15s ease, border-color 0.15s ease;
position: relative;
font-family: inherit;
padding: 0;
@@ -328,19 +326,16 @@ export class PeriodicTable {
// Hover/focus: show tooltip
const activate = () => {
el.style.transform = 'translateY(-2px)';
el.style.boxShadow = `0 4px 12px ${colors.border}40`;
this.showTooltip(el, cmd);
if (this.activeElement && this.activeElement !== el) {
this.activeElement.style.transform = 'translateY(0)';
this.activeElement.style.boxShadow = 'none';
}
this.activeElement = el;
};
const deactivate = () => {
el.style.transform = 'translateY(0)';
el.style.boxShadow = 'none';
this.hideTooltip();
};
+14 -3
View File
@@ -9,6 +9,11 @@ let sourceCache = {}; // Cache fetched source content
const MOBILE_BREAKPOINT = 900;
// Setup / management commands that aren't "steering" verbs. They're kept off
// the command palette (fisheye + mobile carousel) but still appear in the
// periodic table (rendered separately by framework-viz.js).
const PALETTE_EXCLUDED = new Set(['impeccable', 'init', 'extract', 'document', 'live']);
function isMobile() {
return window.innerWidth <= MOBILE_BREAKPOINT;
}
@@ -75,7 +80,7 @@ function renderDesktopLayout(container, commands) {
// too (when they were rendered as 'impeccable craft' etc.) but are now
// first-class sub-commands that should appear in the gallery.
const deprecated = new Set(['teach-impeccable', 'frontend-design', 'arrange', 'normalize', 'onboard', 'impeccable craft', 'impeccable teach', 'impeccable extract']);
const filteredCommands = commands.filter(c => !deprecated.has(c.id));
const filteredCommands = commands.filter(c => !deprecated.has(c.id) && !PALETTE_EXCLUDED.has(c.id));
const categoryOrder = ['create', 'evaluate', 'refine', 'simplify', 'harden', 'system'];
const categoryLabelsShort = {
@@ -308,7 +313,7 @@ function setupFisheyeList(commands, headerIndices = []) {
// to a fractional "center index" which drives everything.
const BASE_H = 36; // height of the center (scale=1) item
const MIN_SCALE = 0.35;
const MIN_SCALE = 0.52; // off-center items stay legibly sized, not microscopic
const RADIUS = 5;
const count = items.length;
const listH = list.clientHeight;
@@ -367,7 +372,10 @@ function setupFisheyeList(commands, headerIndices = []) {
items.forEach((item, i) => {
const h = heights[i];
const scale = getScale(Math.abs(i - center));
const opacity = 0.25 + (scale - MIN_SCALE) / (1 - MIN_SCALE) * 0.75;
// Floor at 0.62 so off-center command names stay readable (WCAG): the
// full vocabulary is the point of this view. Focus still reads clearly
// via scale + the gold/weight active state, not by crushing legibility.
const opacity = 0.62 + (scale - MIN_SCALE) / (1 - MIN_SCALE) * 0.38;
item.style.top = `${y}px`;
item.style.transform = `scale(${scale})`;
@@ -478,6 +486,9 @@ function truncateDescription(text, maxLen = 120) {
// ============================================
function renderMobileLayout(container, commands) {
// Keep setup/management commands off the palette (they stay in the periodic
// table); match the desktop fisheye filter.
commands = commands.filter(c => !PALETTE_EXCLUDED.has(c.id));
// Build carousel pills
// Carousel pills show bare command names for sub-commands, and /impeccable
// for the root entry.
+7 -7
View File
@@ -34,13 +34,13 @@ export const skillFocusAreas = {
// detail line communicates what bad-default we catch — not what we teach.
export const slopFocusAreas = {
'impeccable': [
{ area: 'Typography', detail: 'No Inter monoculture. No flat hierarchy. No all-caps body.' },
{ area: 'Color & Contrast', detail: 'No purple gradients. No sub-WCAG text. No washed pastels.' },
{ area: 'Spatial Design', detail: 'No cards in cards. No identical grids. No template layouts.' },
{ area: 'Responsive', detail: 'No stiff breakpoints. No mobile-as-afterthought.' },
{ area: 'Interaction', detail: 'No modal abuse. No generic CTAs. No gradient text.' },
{ area: 'Motion', detail: 'No bouncy easing. No decorative fades. No spring overshoot.' },
{ area: 'UX Writing', detail: 'No "Welcome to our platform." No em dashes. No "let\'s dive in".' }
{ area: 'Typography', detail: 'A type scale with real contrast, set in a face chosen on purpose, not Inter by reflex.' },
{ area: 'Color & Contrast', detail: 'Palettes that clear WCAG and commit to a hue. The purple-gradient default never ships.' },
{ area: 'Spatial Design', detail: 'Structure from spacing and rhythm, not a grid of identical cards nested in more cards.' },
{ area: 'Responsive', detail: 'Built for the small screen first, with breakpoints that follow the content.' },
{ area: 'Interaction', detail: 'Honest states and clear affordances, without defaulting to a modal or gradient text.' },
{ area: 'Motion', detail: 'Motion that eases like physics and settles. Nothing bounces or springs past its mark.' },
{ area: 'UX Writing', detail: 'Specific copy that names what the thing does, not "Welcome to our platform."' }
]
};
+22 -34
View File
@@ -1,56 +1,44 @@
// Theme toggle - supports light, dark, and system preference
// Theme toggle light / dark with localStorage persistence.
const STORAGE_KEY = 'impeccable-theme';
function getStoredTheme() {
export function getStoredTheme() {
return localStorage.getItem(STORAGE_KEY);
}
function setStoredTheme(theme) {
export function setStoredTheme(theme) {
localStorage.setItem(STORAGE_KEY, theme);
}
function applyTheme(theme) {
export function applyTheme(theme) {
const html = document.documentElement;
const resolved = theme === 'light' ? 'light' : 'dark';
// Remove both classes first
html.classList.remove('light', 'dark');
html.classList.add(resolved);
if (theme === 'light') {
html.classList.add('light');
} else if (theme === 'dark') {
html.classList.add('dark');
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) {
meta.setAttribute('content', resolved === 'light' ? '#f7f4ef' : '#010101');
}
// 'system' = no class, falls back to media query
// Update active state on buttons
document.querySelectorAll('.theme-toggle-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === theme);
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
const next = resolved === 'light' ? 'dark' : 'light';
btn.setAttribute('aria-label', resolved === 'light' ? 'Switch to dark mode' : 'Switch to light mode');
btn.setAttribute('title', resolved === 'light' ? 'Dark mode' : 'Light mode');
btn.dataset.nextTheme = next;
});
}
export function initThemeToggle() {
const toggle = document.querySelector('.theme-toggle');
if (!toggle) return;
const stored = getStoredTheme();
applyTheme(stored === 'light' ? 'light' : 'dark');
// Get stored theme or default to system
const storedTheme = getStoredTheme() || 'system';
applyTheme(storedTheme);
// Handle button clicks
toggle.addEventListener('click', (e) => {
const btn = e.target.closest('.theme-toggle-btn');
if (!btn) return;
const theme = btn.dataset.theme;
setStoredTheme(theme);
applyTheme(theme);
});
// Listen for system preference changes (only matters when in 'system' mode)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (getStoredTheme() === 'system' || !getStoredTheme()) {
applyTheme('system');
}
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
btn.addEventListener('click', () => {
const next = btn.dataset.nextTheme || (document.documentElement.classList.contains('light') ? 'dark' : 'light');
setStoredTheme(next);
applyTheme(next);
});
});
}
+10 -8
View File
@@ -98,10 +98,11 @@
.cf-items strong { color: var(--ks-champagne); font-weight: 600; }
.cf-items code {
font-family: var(--ks-mono);
font-size: 0.92em;
color: var(--ks-kinpaku);
background: transparent;
padding: 0;
font-size: 0.84em;
color: var(--ks-code-fg);
background: var(--ks-code-bg);
padding: var(--ks-code-pad);
border-radius: var(--ks-code-radius);
}
.cf-items a {
color: var(--ks-patina);
@@ -329,10 +330,11 @@
.cf-faq-answer strong { color: var(--ks-champagne); font-weight: 600; }
.cf-faq-answer code {
font-family: var(--ks-mono);
font-size: 0.92em;
color: var(--ks-kinpaku);
background: transparent;
padding: 0;
font-size: 0.84em;
color: var(--ks-code-fg);
background: var(--ks-code-bg);
padding: var(--ks-code-pad);
border-radius: var(--ks-code-radius);
}
.cf-faq-answer a {
color: var(--ks-patina);
+12 -1
View File
@@ -46,11 +46,22 @@
max-width: 100%;
}
.kinpaku-system-page a {
.kinpaku-system-page a:not(.ks-brand) {
color: inherit;
text-decoration: none;
}
/* Brand lockup — kinpaku gold on dark surfaces; html.light swaps to ink
(see light-mode.css) to match impeccable-logo-on-light.svg. */
.kinpaku-system-page a.ks-brand {
color: var(--ks-kinpaku);
}
.kinpaku-system-page a.ks-brand .ks-mark,
.kinpaku-system-page a.ks-brand .ks-wordmark {
color: inherit;
}
.kinpaku-system-page button,
.kinpaku-system-page input,
.kinpaku-system-page select {
+125 -56
View File
@@ -47,10 +47,12 @@
.designing-kinpaku code {
font-family: var(--ks-mono);
color: var(--ks-kinpaku);
background: transparent;
font-size: 0.84em;
color: var(--ks-code-fg);
background: var(--ks-code-bg);
border: 0;
padding: 0;
padding: var(--ks-code-pad);
border-radius: var(--ks-code-radius);
}
/* ============================================================================
@@ -272,8 +274,10 @@
.designing-kinpaku .designing-loop-name {
color: var(--ks-champagne);
font-family: var(--ks-font-display);
font-weight: 300;
/* Pinstripe is single-weight and reads broken at ~1.52rem; wordmark
carries real stroke weight at compass-label scale (cf. iterate-name). */
font-family: var(--ks-font-wordmark);
font-weight: 600;
font-size: clamp(1.5rem, 2.2vw, 2rem);
line-height: 1.05;
letter-spacing: 0.01em;
@@ -444,7 +448,7 @@
============================================================================ */
.designing-kinpaku .designing-phase-sub code {
color: var(--ks-kinpaku);
color: var(--ks-code-fg);
}
.designing-kinpaku .designing-phase-commands {
@@ -459,7 +463,7 @@
display: inline-flex;
align-items: center;
padding: 8px 16px;
background: oklch(11% 0.006 95);
background: oklch(11% 0 0);
border: 1px solid var(--ks-rule);
border-radius: 2px;
color: var(--ks-kinpaku);
@@ -530,7 +534,7 @@
.designing-kinpaku .docs-viz-file-header {
padding: 14px 18px;
border-bottom: 1px solid var(--ks-rule);
background: oklch(13% 0.006 95);
background: oklch(13% 0 0);
display: flex;
align-items: center;
justify-content: space-between;
@@ -740,7 +744,7 @@
frame opposite it. */
.designing-kinpaku .designing-iterate-terminal {
padding: 20px 22px;
background: oklch(13% 0.006 95);
background: oklch(13% 0 0);
border: 1px solid var(--ks-rule);
border-radius: 4px;
font-family: var(--ks-mono);
@@ -763,7 +767,7 @@
/* Live frame mock — restyle docs-viz-live-* on dark surface */
.designing-kinpaku .docs-viz-live-frame {
background: oklch(13% 0.006 95);
background: oklch(13% 0 0);
border: 1px solid var(--ks-rule);
border-radius: 4px;
overflow: hidden;
@@ -774,7 +778,7 @@
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid var(--ks-rule);
background: oklch(11% 0.006 95);
background: oklch(11% 0 0);
}
.designing-kinpaku .docs-viz-live-dot {
width: 9px;
@@ -804,7 +808,7 @@
}
.designing-kinpaku .docs-viz-live-target {
width: 100%;
background: oklch(11% 0.006 95);
background: oklch(11% 0 0);
border: 1px solid var(--ks-rule);
border-radius: 4px;
padding: 20px;
@@ -832,7 +836,7 @@
.designing-kinpaku .docs-viz-live-btn {
align-self: start;
padding: 8px 14px;
background: var(--ks-kinpaku-pale);
background: var(--ks-kinpaku);
color: var(--ks-lacquer-deep);
border: 0;
border-radius: 2px;
@@ -846,8 +850,7 @@
position: absolute;
inset: 14px 14px 108px;
border: 1.5px solid var(--ks-kinpaku);
border-radius: 4px;
box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.1);
border-radius: 3px;
pointer-events: none;
}
/* top: auto is required — the base/legacy rule sets top: 16px, and leaving it
@@ -863,11 +866,9 @@
gap: 6px;
padding: 6px;
background: var(--ks-lacquer-deep);
border: 1.5px solid var(--ks-kinpaku);
border-radius: 6px;
box-shadow:
0 0 0 1px oklch(78% 0.12 82 / 0.18),
0 6px 18px oklch(0% 0 0 / 0.4);
border: 1px solid oklch(92% 0 0 / 0.13);
border-radius: 8px;
box-shadow: 0 6px 18px -4px oklch(0% 0 0 / 0.5);
}
.designing-kinpaku .docs-viz-live-ctx-nav,
.designing-kinpaku .docs-viz-live-ctx-discard,
@@ -885,7 +886,7 @@
padding: 4px 6px;
}
.designing-kinpaku .docs-viz-live-ctx-accept {
background: var(--ks-kinpaku-pale);
background: var(--ks-kinpaku);
color: var(--ks-lacquer-deep);
border-radius: 3px;
font-weight: 500;
@@ -916,11 +917,9 @@
padding: 5px 7px;
max-width: calc(100% - 24px);
background: var(--ks-lacquer-deep);
border: 1.5px solid var(--ks-kinpaku);
border-radius: 9px;
box-shadow:
0 0 0 1px oklch(78% 0.12 82 / 0.18),
0 10px 28px oklch(0% 0 0 / 0.28);
border: 1px solid oklch(92% 0 0 / 0.13);
border-radius: 8px;
box-shadow: 0 10px 28px -6px oklch(0% 0 0 / 0.5);
}
.designing-kinpaku .docs-viz-live-gbar-mark {
display: inline-flex;
@@ -944,7 +943,7 @@
flex-shrink: 0;
}
.designing-kinpaku .docs-viz-live-gbar-btn.is-active {
background: oklch(78% 0.12 82 / 0.18);
background: oklch(27% 0 0);
color: var(--ks-kinpaku);
}
.designing-kinpaku .docs-viz-live-gbar-btn-mono {
@@ -1029,13 +1028,12 @@
the 3-col grid below, not as a card competing with them. */
.designing-kinpaku .designing-polish-band {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-columns: auto 1fr;
align-items: center;
gap: 20px;
padding: 18px 0;
background: transparent;
border: 0;
border-top: 1px solid var(--ks-rule);
border-bottom: 1px solid var(--ks-rule);
border-radius: 0;
margin-bottom: 36px;
@@ -1061,16 +1059,18 @@
text-underline-offset: 4px;
}
.designing-kinpaku .designing-polish-band-meta {
color: var(--ks-muted);
font-family: var(--ks-mono);
font-size: 0.7rem;
letter-spacing: 0.18em;
display: none; /* cryptic "03 · 04" step counter — removed */
}
.designing-kinpaku .designing-polish-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
/* Flatten the legacy outer box (docs-visuals adds a cream bg + L/R/B border +
padding); the cards are the only surface, no box-in-box. */
background: none;
border: 0;
padding: 0;
}
@media (max-width: 900px) {
.designing-kinpaku .designing-polish-grid { grid-template-columns: 1fr; }
@@ -1131,7 +1131,7 @@
gap: 10px;
margin: 2px 0;
padding: 11px 14px;
background: oklch(13% 0.006 95);
background: oklch(13% 0 0);
border: 1px solid var(--ks-rule);
border-radius: 4px;
font-family: var(--ks-mono);
@@ -1150,10 +1150,11 @@
}
.designing-kinpaku .designing-maintain-stage {
background: var(--ks-lacquer-raised);
border: 1px solid var(--ks-rule);
border-radius: 4px;
padding: 28px;
/* No inner box — the demo sits directly in the bento tile (removing the
box-in-box: plinth > tile > stage). */
background: transparent;
border: 0;
padding: 8px 0;
min-height: 180px;
display: grid;
place-items: center;
@@ -1211,7 +1212,7 @@
display: inline-flex;
align-items: center;
padding: 5px 11px;
background: oklch(13% 0.006 95);
background: oklch(13% 0 0);
border: 1px solid var(--ks-rule);
border-radius: 3px;
color: var(--ks-muted);
@@ -1226,7 +1227,7 @@
.designing-kinpaku .designing-extract-after {
justify-self: end;
padding: 8px 18px;
background: var(--ks-kinpaku-pale);
background: var(--ks-kinpaku);
color: var(--ks-lacquer-deep);
border-radius: 2px;
font-family: var(--ks-font);
@@ -1269,6 +1270,26 @@
margin-bottom: 24px;
}
/* Flatten the bento plinth: drop the 0.17 plinth fill + 8px gutter (which drew
the weird gutter "borders") and the 0.07 tile fill. The two lanes sit on the
page, split by a single center hairline. */
.designing-kinpaku .ks-bento.designing-lanes {
background: transparent;
border: 0;
border-radius: 0;
gap: 0;
}
.designing-kinpaku .designing-lanes .ks-bento-tile {
background: transparent;
padding: 0 clamp(20px, 3.5vw, 44px);
}
.designing-kinpaku .designing-lanes .ks-bento-tile:first-child {
padding-left: 0;
}
.designing-kinpaku .designing-lanes .ks-bento-tile + .ks-bento-tile {
border-left: 1px solid var(--ks-rule);
}
.designing-kinpaku .designing-lane-kind {
color: var(--ks-kinpaku);
font-family: var(--ks-mono);
@@ -1283,41 +1304,85 @@
.designing-kinpaku .designing-lane-rule {
color: var(--ks-text);
font-size: 1rem;
line-height: 1.6;
line-height: 1.65;
margin: 0;
text-wrap: pretty;
}
.designing-kinpaku .designing-lane-rule strong {
color: var(--ks-champagne);
font-weight: 600;
}
/* The brand mock title used the pinstripe display face at ~1.6rem, which reads
broken/washed at that size (Two-Face rule); use the clean body face. */
.designing-kinpaku .designing-lane-mock-title {
font-family: var(--ks-font);
font-weight: 500;
}
.designing-kinpaku .designing-lane-mock {
margin-top: 6px;
padding: 24px;
background: oklch(11% 0.006 95);
margin-top: 18px;
padding: 20px 18px;
background: var(--ks-lacquer-raised);
border: 1px solid var(--ks-rule);
border-radius: 4px;
min-height: 110px;
min-height: 120px;
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
}
.designing-kinpaku .designing-lane-mock--brand {
align-items: flex-start;
}
.designing-kinpaku .designing-lane-mock--product {
align-items: flex-start;
align-items: stretch;
padding: 0;
gap: 0;
}
.designing-kinpaku .designing-lane-mock-label {
color: var(--ks-patina);
font-family: var(--ks-mono);
font-size: 0.66rem;
font-size: 0.62rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.designing-kinpaku .designing-lane-mock--brand .designing-lane-mock-title {
color: var(--ks-champagne);
font-family: var(--ks-font-display);
font-weight: 300;
font-size: 1.6rem;
font-family: var(--ks-font);
font-weight: 500;
font-size: 1.35rem;
line-height: 1.15;
}
.designing-kinpaku .designing-lane-mock-accent {
color: var(--ks-kinpaku);
}
.designing-kinpaku .designing-lane-product-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid var(--ks-rule);
}
.designing-kinpaku .designing-lane-product-row:last-child {
border-bottom: 0;
}
.designing-kinpaku .designing-lane-product-k {
font-family: var(--ks-mono);
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ks-text-muted);
}
.designing-kinpaku .designing-lane-product-v {
font-family: var(--ks-font);
font-size: 0.92rem;
font-weight: 500;
color: var(--ks-champagne);
}
/* Legacy single-line product mock title — kept for any downstream refs. */
.designing-kinpaku .designing-lane-mock--product .designing-lane-mock-title {
color: var(--ks-champagne);
font-family: var(--ks-font);
@@ -1344,16 +1409,20 @@
margin: 0;
padding: 0;
display: grid;
gap: 18px;
gap: 0; /* divided list: items separated by hairlines, not gaps */
}
.designing-kinpaku .designing-avoid li {
display: grid;
grid-template-columns: 28px 1fr;
gap: 14px;
padding: 22px 24px;
background: var(--ks-lacquer-raised);
border: 1px solid var(--ks-rule);
border-radius: 2px;
padding: 22px 0;
background: transparent;
border: 0;
border-bottom: 1px solid var(--ks-rule);
border-radius: 0;
}
.designing-kinpaku .designing-avoid li:last-child {
border-bottom: 0;
}
.designing-kinpaku .designing-avoid-x {
color: var(--ks-vermilion);
@@ -1370,7 +1439,7 @@
font-size: 1rem;
margin-bottom: 6px;
}
.designing-kinpaku .designing-avoid-title code { color: var(--ks-kinpaku); }
.designing-kinpaku .designing-avoid-title code { color: var(--ks-code-fg); }
.designing-kinpaku .designing-avoid-desc {
color: var(--ks-text);
font-size: 0.94rem;
+2 -2
View File
@@ -396,7 +396,7 @@
padding: 0;
overflow: hidden;
background: transparent;
color: var(--ks-champagne);
color: var(--ks-code-fg);
font-family: var(--ks-mono);
text-overflow: ellipsis;
white-space: nowrap;
@@ -471,7 +471,7 @@
.detector-table-panel td code {
padding: 0;
background: transparent;
color: var(--ks-champagne);
color: var(--ks-code-fg);
font-family: var(--ks-mono);
font-size: 0.8125rem;
}
+14 -10
View File
@@ -1044,17 +1044,17 @@
.docs-kinpaku .prose code {
font-family: var(--ks-mono);
font-size: 0.84em;
color: var(--ks-champagne);
background: var(--ks-graphite-2);
color: var(--ks-code-fg);
background: var(--ks-code-bg);
border: 0;
padding: 4px 7px;
border-radius: 3px;
padding: var(--ks-code-pad);
border-radius: var(--ks-code-radius);
}
/* Inline code that is itself a link get the kinpaku cue back, since
the link semantic is the meaningful one. */
.docs-kinpaku .prose a code {
color: var(--ks-kinpaku);
color: var(--ks-code-cmd);
}
/* Fenced code blocks deep lacquer terminal. Shiki injects inline
@@ -1067,10 +1067,10 @@
.docs-kinpaku .prose pre.astro-code {
margin: 0;
padding: 22px 24px;
background: var(--ks-lacquer-deep) !important;
color: oklch(86% 0.025 82) !important;
border: 1px solid var(--ks-rule);
border-radius: 2px;
background: var(--ks-code-block-bg) !important;
color: var(--ks-code-block-fg) !important;
border: 1px solid var(--ks-code-block-border);
border-radius: var(--ks-code-block-radius);
overflow-x: auto;
font-family: var(--ks-mono);
font-size: 0.84rem;
@@ -1227,7 +1227,7 @@
background: var(--ks-lacquer-raised);
}
.docs-kinpaku .docs-command-demo .split-comparison {
.docs-kinpaku .docs-command-demo :is(.split-comparison, .demo-split-comparison) {
max-width: none;
margin: 0;
padding: 0;
@@ -1253,6 +1253,9 @@
}
.docs-kinpaku .docs-command-demo .split-after {
/* Doc.astro initSplitCompare uses skewAngle: 0 vertical seam, not the
angled default from sub-pages.css / main.css. */
clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%);
background:
linear-gradient(90deg, oklch(8% 0.006 95 / 0.92), oklch(10% 0.006 95 / 0.96)),
radial-gradient(circle at 82% 26%, oklch(78% 0.12 82 / 0.14), transparent 12rem);
@@ -1261,6 +1264,7 @@
.docs-kinpaku .docs-command-demo .split-divider {
background: var(--ks-kinpaku);
box-shadow: 0 0 20px oklch(78% 0.12 82 / 0.4);
transform: translateX(-50%) !important;
}
.docs-kinpaku .docs-command-demo .demo-caption {
+4 -4
View File
@@ -3347,10 +3347,10 @@
.neon-case-command .code-block {
margin: 0;
padding: var(--spacing-md);
background: var(--ks-lacquer);
color: var(--ks-champagne);
border: 1px solid var(--ks-rule);
border-radius: 2px;
background: var(--ks-code-block-bg);
color: var(--ks-code-block-fg);
border: 1px solid var(--ks-code-block-border);
border-radius: var(--ks-code-block-radius);
overflow-x: auto;
font-family: var(--ks-mono);
font-size: 0.8125rem;
+247 -185
View File
@@ -6,18 +6,12 @@
@import "./kinpaku-tokens.css";
.home-kinpaku {
/* Intentional overrides: the homepage uses brighter rule alphas because
bento tile borders need to read against busy backgrounds. The default
--ks-rule from kinpaku-tokens.css is the quieter doctrinal value used
on /design-system and elsewhere. */
--ks-rule: oklch(58% 0.065 82 / 0.48);
--ks-rule-strong: oklch(74% 0.09 82 / 0.68);
/* --ks-rule / --ks-rule-strong inherit the global tokens (neutral default,
gold strong); the homepage no longer diverges on border color. */
/* Slightly lighter muted text on the homepage's busier surfaces. The
default --ks-text-muted (63% L) is used everywhere else; here we lift
to 65% so captions stay readable against bento tile fills. Kept under
the old --ks-muted name for code-churn reasons. */
--ks-muted: oklch(65% 0.024 82);
/* Legacy alias homepage code reads --ks-muted; map it to the global muted
text token (no divergent value). */
--ks-muted: var(--ks-text-muted);
/* Legacy alias homepage code still reads --ks-faint and --ks-oxide. The
forward names live in kinpaku-tokens.css as --ks-text-faint and
@@ -98,12 +92,13 @@
(3) keep the chip treatment subtle so it doesn't drag attention off the
prose. */
.home-kinpaku code {
color: oklch(82% 0.11 82);
background: oklch(78% 0.12 82 / 0.08);
border: 1px solid oklch(78% 0.12 82 / 0.14);
border-radius: 3px;
color: var(--ks-code-fg);
background: var(--ks-code-bg);
border: 0;
border-radius: var(--ks-code-radius);
font-family: var(--ks-mono);
font-size: 0.88em;
padding: 0.05em 0.4em;
padding: var(--ks-code-pad);
}
.home-kinpaku ::selection {
@@ -214,7 +209,7 @@
.home-kinpaku .section-lead,
.home-kinpaku .section-subtitle {
max-width: 68ch;
max-width: 62ch; /* 68ch rendered ~86 actual chars/line; tighten into the band */
color: var(--ks-text);
font-size: 1.02rem;
line-height: 1.8;
@@ -233,7 +228,6 @@
.home-kinpaku .downloads-rebuild-note,
.home-kinpaku .why-panel-title,
.home-kinpaku .live-demo-support-v,
.home-kinpaku .downloads-rebuild-examples span,
.home-kinpaku .downloads-rebuild-extras strong,
.home-kinpaku .downloads-rebuild-extras span {
text-wrap: balance;
@@ -907,7 +901,10 @@
padding: 24px;
border: 1px solid var(--ks-rule);
border-radius: 4px;
background: oklch(9% 0.006 95 / 0.92);
/* Was 9% on a 7% ground (~1 level in sRGB): the card body vanished into the
page and only the hairline hinted at an edge. Lift to a clear raised
neutral tone so the specimen card reads as a crisp object. */
background: oklch(15% 0 0);
}
.home-kinpaku .foundation-column:hover .foundation-card {
@@ -940,15 +937,11 @@
.home-kinpaku .foundation-plinth {
border-color: var(--ks-rule);
/* Neutral hatch (was kinpaku gold, which washed the section champagne) on a
neutral base, for a crisp pedestal that matches the de-warmed section. */
background:
repeating-linear-gradient(45deg, oklch(78% 0.12 82 / 0.12), oklch(78% 0.12 82 / 0.12) 1px, transparent 1px, transparent 9px),
oklch(10% 0.006 95 / 0.42);
/* Soften the bottom so the plinth fades into the section bg like a real
shadow instead of terminating in a hard horizontal line. Mask fades the
bottom 45% of the element, taking the diagonal stripes + side borders
down with it. */
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%);
mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%);
repeating-linear-gradient(45deg, oklch(80% 0 0 / 0.07), oklch(80% 0 0 / 0.07) 1px, transparent 1px, transparent 9px),
oklch(10% 0 0 / 0.42);
}
.home-kinpaku .language-intro-row {
@@ -982,6 +975,8 @@
surface (flat oklch(0.17 0 0), no border, no shadow). All inner demo
surfaces sit transparently on top no plinth-in-plinth, no nested rings. */
.home-kinpaku .magazine-container {
/* Solid panel, no border. The demo preview inside gets its own thin border
(see .spread-demo-area below) and inherits this fill. */
border: 0;
border-radius: 2px;
background: oklch(0.17 0 0);
@@ -989,7 +984,7 @@
}
.home-kinpaku .fisheye-item {
color: var(--ks-muted);
color: var(--ks-text);
font-family: var(--ks-font);
font-size: 1.18rem;
}
@@ -1018,13 +1013,23 @@
}
.home-kinpaku .spread-flow-cmd {
color: var(--ks-kinpaku);
background: oklch(78% 0.12 82 / 0.08);
border: 1px solid oklch(78% 0.12 82 / 0.18);
color: var(--ks-code-cmd);
background: var(--ks-code-bg);
border: 0;
border-radius: var(--ks-code-radius);
}
/* Inner demo surfaces are transparent the plinth lives on the outer
.magazine-container; these sit on top with no chrome of their own. */
/* The demo's frame is the .split-container's own border; the caption sits
outside it. Strip the leftover chrome: no grid ::before, no inner shadow. */
.home-kinpaku .spread-demo-area .split-container {
box-shadow: none;
}
.home-kinpaku .spread-demo-area .split-container::before {
display: none;
}
/* Inner demo surfaces are transparent they sit on the panel inside the
framed .split-container, with no chrome of their own. */
.home-kinpaku .spread-demo-area .demo-split-comparison .split-container,
.home-kinpaku .terminal-preview .demo-split-comparison .split-container,
.home-kinpaku .demo-container,
@@ -1115,14 +1120,16 @@
--ptable-accent: var(--ks-kinpaku);
width: clamp(64px, 5vw, 76px) !important;
height: clamp(76px, 5.8vw, 90px) !important;
border-width: 1px !important;
/* Crisp neutral card. Was a category-tinted gradient bg + strong colored
border + muddy multi-inset shadow that read as dim blobs; now a clean
graphite tile with a neutral hairline. Category is cued by the group label
and the accent line below, not a muddy fill. */
background: oklch(15% 0 0) !important;
border: 1px solid var(--ks-rule) !important;
border-radius: 4px !important;
overflow: hidden;
isolation: isolate;
box-shadow:
inset 0 0 0 1px oklch(100% 0 0 / 0.035),
inset 0 -18px 34px oklch(2% 0.004 95 / 0.28),
0 18px 42px oklch(2% 0.004 95 / 0.22) !important;
box-shadow: none !important;
}
.home-kinpaku #framework-viz-container .ptable-element--create {
@@ -1149,40 +1156,20 @@
--ptable-accent: var(--cat-system-text);
}
/* No gold-leaf texture overlay (was washing the tile bg) and no bottom accent
line the tile is a clean flat neutral surface. */
.home-kinpaku #framework-viz-container .ptable-element::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background:
radial-gradient(circle at 74% 12%, oklch(100% 0 0 / 0.09), transparent 32%),
url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") center / 190px auto;
mix-blend-mode: screen;
opacity: 0.055;
pointer-events: none;
content: none;
}
.home-kinpaku #framework-viz-container .ptable-element::after {
content: "";
position: absolute;
left: 12px;
right: 12px;
bottom: 9px;
z-index: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--ptable-accent), transparent);
opacity: 0.24;
pointer-events: none;
content: none;
}
.home-kinpaku #framework-viz-container .ptable-element:is(:hover, :focus-visible) {
transform: translateY(-3px) !important;
border-color: var(--ptable-accent) !important;
box-shadow:
inset 0 0 0 1px oklch(100% 0 0 / 0.05),
0 0 0 1px oklch(78% 0.12 82 / 0.12),
0 22px 48px oklch(2% 0.004 95 / 0.36),
0 0 26px oklch(78% 0.12 82 / 0.16) !important;
border-color: oklch(85% 0 0 / 0.4) !important;
box-shadow: none !important;
}
.home-kinpaku #framework-viz-container .ptable-number,
@@ -1201,16 +1188,17 @@
.home-kinpaku #framework-viz-container .ptable-symbol {
font-size: clamp(1.45rem, 1.8vw, 1.8rem) !important;
font-weight: 300 !important;
font-weight: 400 !important;
letter-spacing: 0.01em;
color: var(--ks-champagne) !important;
}
.home-kinpaku #framework-viz-container .ptable-name {
max-width: 64px !important;
color: var(--ptable-accent) !important;
font-size: 0.54rem !important;
color: var(--ks-text-muted) !important;
font-size: 0.56rem !important;
letter-spacing: 0.04em;
opacity: 0.72 !important;
opacity: 1 !important;
}
.home-kinpaku #framework-viz-container .ptable-alpha {
@@ -1222,12 +1210,14 @@
.home-kinpaku .spread-demo-area .demo-split-comparison .split-before,
.home-kinpaku .terminal-preview .demo-split-comparison .split-before {
background: oklch(10% 0.006 95);
/* Left half inherits the panel fill — no separate background. */
background: transparent;
}
.home-kinpaku .spread-demo-area .demo-split-comparison .split-after,
.home-kinpaku .terminal-preview .demo-split-comparison .split-after {
background: oklch(8% 0.006 95);
/* Right half is near-black: the clean before/after contrast. */
background: oklch(2% 0 0);
}
.home-kinpaku .demo-caption,
@@ -1262,12 +1252,12 @@
}
.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-container {
background:
linear-gradient(90deg, oklch(78% 0.12 82 / 0.028) 1px, transparent 1px),
linear-gradient(180deg, oklch(78% 0.12 82 / 0.022) 1px, transparent 1px),
radial-gradient(circle at 74% 48%, oklch(70% 0.105 190 / 0.055), transparent 18rem),
oklch(7% 0.005 95);
background-size: 36px 36px, 36px 36px, auto, auto;
/* No grid lines, no radial glow the container is transparent so the before
half shows the panel fill and the after half its own near-black. A thin
neutral border frames the demo itself (the caption sits outside it). */
background: transparent;
border: 1px solid var(--ks-rule);
border-radius: 4px;
}
.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-before,
@@ -1490,7 +1480,6 @@
dropped, the bento structure carries the brand voice now. */
.home-kinpaku .why-terminal,
.home-kinpaku .why-productmd-file,
.home-kinpaku .why-register,
.home-kinpaku .why-ci-window,
.home-kinpaku .why-browser,
.home-kinpaku .why-dm-grid,
@@ -1501,6 +1490,104 @@
border-radius: 4px;
}
.home-kinpaku .why-register {
background: transparent;
border: 0;
border-radius: 0;
gap: 10px;
}
.home-kinpaku .why-register-label {
color: var(--ks-text-muted);
font-family: var(--ks-mono);
font-size: 0.62rem;
letter-spacing: 0.2em;
}
.home-kinpaku .why-register-mock--brand,
.home-kinpaku .why-register-mock--product {
border: 1px solid var(--ks-rule);
border-radius: 4px;
background: var(--ks-lacquer-raised);
}
.home-kinpaku .why-brand-hero-mono {
color: var(--ks-text-muted);
}
.home-kinpaku .why-brand-hero-title {
color: var(--ks-champagne);
font-family: var(--ks-font-wordmark);
font-weight: 300;
}
.home-kinpaku .why-brand-hero-title em {
color: var(--ks-kinpaku);
font-style: italic;
}
.home-kinpaku .why-product-k {
color: var(--ks-text-muted);
}
.home-kinpaku .why-product-v {
color: var(--ks-champagne);
}
.home-kinpaku .why-product-row {
border-color: var(--ks-rule);
}
.home-kinpaku .why-productmd-file,
.home-kinpaku .why-terminal {
overflow: hidden;
border: 1px solid var(--ks-rule);
}
.home-kinpaku .why-productmd-header,
.home-kinpaku .why-productmd-footer,
.home-kinpaku .why-terminal-header {
border-radius: 0;
}
/* Bento window chrome dots — one subtle traffic-light set across mocks. */
.home-kinpaku :is(
.why-terminal-dot,
.why-live-dot,
.why-visual--extension .why-browser-dot,
.why-visual--v2 .v2-new-browser-dot
) {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
opacity: 0.5;
background: oklch(58% 0.012 95);
}
.home-kinpaku :is(
.why-terminal-dot,
.why-live-dot,
.why-visual--extension .why-browser-dot,
.why-visual--v2 .v2-new-browser-dot
):nth-child(1) {
background: var(--ks-kinpaku-deep);
}
.home-kinpaku :is(
.why-terminal-dot,
.why-live-dot,
.why-visual--extension .why-browser-dot,
.why-visual--v2 .v2-new-browser-dot
):nth-child(2) {
background: var(--ks-patina-deep);
}
.home-kinpaku .why-terminal-header,
.home-kinpaku .why-live-chrome {
gap: 5px;
}
.home-kinpaku .why-panel-title {
color: var(--ks-champagne);
font-size: 2rem;
@@ -1519,8 +1606,7 @@
.home-kinpaku .why-productmd-k,
.home-kinpaku .why-terminal-ok,
.home-kinpaku .why-terminal-path,
.home-kinpaku .why-dm-badge,
.home-kinpaku .why-ci-status {
.home-kinpaku .why-dm-badge {
color: var(--ks-patina);
}
@@ -1550,6 +1636,9 @@
.home-kinpaku .why-ci-header {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 54px;
padding: 14px 18px 14px 72px;
border-bottom: 1px solid var(--ks-rule);
@@ -1561,29 +1650,35 @@
position: absolute;
left: 22px;
top: 50%;
width: 34px;
height: 9px;
width: 31px;
height: 7px;
transform: translateY(-50%);
background:
radial-gradient(circle at 4px 50%, var(--ks-kinpaku) 0 3px, transparent 3.5px),
radial-gradient(circle at 17px 50%, var(--ks-patina) 0 3px, transparent 3.5px),
radial-gradient(circle at 30px 50%, var(--ks-kinpaku-deep) 0 3px, transparent 3.5px);
opacity: 0.88;
radial-gradient(circle at 3.5px 50%, var(--ks-kinpaku-deep) 0 3.5px, transparent 3.6px),
radial-gradient(circle at 13.5px 50%, var(--ks-patina-deep) 0 3.5px, transparent 3.6px),
radial-gradient(circle at 23.5px 50%, oklch(58% 0.012 95) 0 3.5px, transparent 3.6px);
opacity: 0.5;
}
.home-kinpaku .why-ci-branch {
font-family: var(--ks-mono);
color: var(--ks-champagne);
font-size: 0.78rem;
letter-spacing: 0.03em;
}
.home-kinpaku .why-ci-status {
.home-kinpaku .why-ci-status,
.home-kinpaku .why-ci-status--fail {
font-family: var(--ks-mono);
font-size: 0.62rem;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 5px 13px;
border: 1px solid oklch(58% 0.15 35 / 0.3);
color: var(--ks-patina);
background:
linear-gradient(90deg, oklch(58% 0.15 35 / 0.22), oklch(58% 0.15 35 / 0.1));
box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.02);
border-radius: 999px;
border: 1px solid oklch(58% 0.15 35 / 0.42);
color: oklch(74% 0.14 35);
background: oklch(58% 0.15 35 / 0.14);
box-shadow: none;
}
.home-kinpaku .why-ci-body {
@@ -1652,9 +1747,13 @@
}
.home-kinpaku .why-visual--live {
padding: 0;
flex: 1;
padding: clamp(16px, 2vw, 22px);
overflow: hidden;
min-height: 240px;
min-height: min(280px, 42vw);
display: flex;
align-items: stretch;
justify-content: center;
background:
linear-gradient(90deg, oklch(78% 0.12 82 / 0.018) 1px, transparent 1px),
linear-gradient(180deg, oklch(78% 0.12 82 / 0.015) 1px, transparent 1px),
@@ -1665,7 +1764,8 @@
.home-kinpaku .why-live-frame {
position: relative;
margin: 24px clamp(20px, 3vw, 32px) 0;
width: 100%;
margin: 0;
border: 1px solid oklch(78% 0.12 82 / 0.22);
border-radius: 6px;
background:
@@ -1675,25 +1775,17 @@
inset 0 0 0 1px oklch(100% 0 0 / 0.02),
0 18px 50px oklch(2% 0.004 95 / 0.32);
overflow: hidden;
display: flex;
flex-direction: column;
}
.home-kinpaku .why-live-chrome {
display: flex;
align-items: center;
gap: 5px;
padding: 9px 14px;
border-bottom: 1px solid var(--ks-rule);
background: oklch(13% 0.006 95);
}
.home-kinpaku .why-live-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: oklch(58% 0.018 82);
opacity: 0.55;
}
.home-kinpaku .why-live-dot:nth-child(1) { background: var(--ks-kinpaku-deep); }
.home-kinpaku .why-live-dot:nth-child(2) { background: var(--ks-patina-deep); }
.home-kinpaku .why-live-url {
margin-left: 12px;
padding: 3px 10px;
@@ -1707,8 +1799,12 @@
.home-kinpaku .why-live-stage {
position: relative;
min-height: 130px;
padding: 24px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 148px;
padding: 32px 28px;
background:
repeating-linear-gradient(0deg, transparent 0 9px, oklch(100% 0 0 / 0.012) 9px 10px),
oklch(8% 0.006 95);
@@ -1717,9 +1813,9 @@
.home-kinpaku .why-live-pick {
position: relative;
width: max-content;
max-width: calc(100% - 8px);
padding: 18px 22px;
margin: 12px auto 4px;
max-width: min(100%, 320px);
padding: 20px 24px;
margin: 0;
border: 2px solid var(--ks-kinpaku);
border-radius: 3px;
background:
@@ -1732,20 +1828,20 @@
.home-kinpaku .why-live-pick-tag {
display: block;
font-family: var(--ks-mono);
font-size: 0.7rem;
font-size: 0.74rem;
letter-spacing: 0.06em;
color: var(--ks-kinpaku);
}
.home-kinpaku .why-live-pick-note {
position: absolute;
top: -14px;
right: -18px;
padding: 5px 10px;
top: -12px;
right: -14px;
padding: 6px 11px;
background: var(--ks-kinpaku);
border-radius: 2px;
color: oklch(8% 0.006 95);
font-family: var(--ks-mono);
font-size: 0.7rem;
font-size: 0.72rem;
letter-spacing: 0.02em;
white-space: nowrap;
box-shadow: 0 4px 14px oklch(78% 0.12 82 / 0.25);
@@ -1763,46 +1859,35 @@
50% { opacity: 0; }
}
.home-kinpaku .why-live-gbar {
display: flex;
align-items: center;
gap: 10px;
.home-kinpaku .why-live-frame > .why-live-ctx.live-demo-ctx {
position: static;
left: auto;
transform: none;
display: block;
opacity: 1;
z-index: auto;
width: 100%;
max-width: none;
padding: 10px 14px;
border: 0;
border-top: 1px solid var(--ks-rule);
border-radius: 0;
box-shadow: none;
background: oklch(13% 0.006 95);
font-family: var(--ks-mono);
font-size: 0.74rem;
}
.home-kinpaku .why-live-gbar-brand {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--ks-kinpaku);
color: oklch(8% 0.006 95);
border-radius: 3px;
font-weight: 600;
.home-kinpaku .why-live-frame > .why-live-ctx .live-demo-ctx-row--configure {
display: flex;
gap: 6px;
}
.home-kinpaku .why-live-gbar-cmd {
padding: 3px 10px;
border: 1px solid oklch(78% 0.12 82 / 0.3);
border-radius: 3px;
color: var(--ks-kinpaku);
.home-kinpaku .why-live-frame > .why-live-ctx .live-demo-ctx-input {
flex: 1;
min-width: 0;
}
.home-kinpaku .why-live-gbar-count {
padding: 3px 8px;
border: 1px solid var(--ks-rule);
border-radius: 3px;
color: var(--ks-text-muted);
}
.home-kinpaku .why-live-gbar-go {
margin-left: auto;
padding: 4px 12px;
background: var(--ks-kinpaku);
color: oklch(8% 0.006 95);
border-radius: 3px;
font-weight: 500;
.home-kinpaku .why-live-frame > .why-live-ctx .live-demo-ctx-caret {
animation: why-live-caret 1s steps(2, end) infinite;
}
/* ============================================================
@@ -1908,20 +1993,6 @@
linear-gradient(180deg, oklch(13% 0.006 95), oklch(8% 0.005 95));
}
.home-kinpaku .why-visual--extension .why-browser-dot {
width: 8px;
height: 8px;
background: var(--ks-kinpaku-deep);
}
.home-kinpaku .why-visual--extension .why-browser-dot:nth-child(2) {
background: var(--ks-patina);
}
.home-kinpaku .why-visual--extension .why-browser-dot:nth-child(3) {
background: var(--ks-kinpaku);
}
.home-kinpaku .why-visual--extension .why-browser-url {
min-height: 30px;
display: flex;
@@ -2024,14 +2095,11 @@
}
.home-kinpaku .why-visual--designmd-v2 {
gap: 16px;
padding: clamp(18px, 2vw, 26px);
overflow: hidden;
background:
radial-gradient(circle at 13% 24%, oklch(78% 0.12 82 / 0.055), transparent 18rem),
radial-gradient(circle at 88% 18%, oklch(70% 0.105 190 / 0.06), transparent 18rem),
linear-gradient(135deg, oklch(78% 0.12 82 / 0.035), transparent 46%),
oklch(6% 0.005 95 / 0.92);
flex-direction: column;
gap: 14px;
padding: 0;
overflow: visible;
background: transparent;
}
.home-kinpaku .why-dm-header {
@@ -2058,11 +2126,9 @@
}
.home-kinpaku .why-dm-tile {
border-color: var(--ks-rule);
background:
linear-gradient(135deg, oklch(100% 0 0 / 0.024), transparent 38%),
oklch(5% 0.004 95 / 0.78);
box-shadow: inset 0 0 0 1px oklch(78% 0.12 82 / 0.026);
border: 1px solid var(--ks-rule);
background: var(--ks-lacquer-raised);
box-shadow: none;
}
.home-kinpaku .why-dm-tile-meta {
@@ -2298,20 +2364,14 @@
}
.home-kinpaku .why-visual--v2 .v2-new-browser-dots {
display: flex;
gap: 5px;
border-bottom: 1px solid var(--ks-rule);
background:
linear-gradient(90deg, oklch(78% 0.12 82 / 0.08), transparent),
oklch(8% 0.006 95);
}
.home-kinpaku .why-visual--v2 .v2-new-browser-dot {
background: var(--ks-kinpaku-deep);
}
.home-kinpaku .why-visual--v2 .v2-new-browser-dot:nth-child(2) {
background: var(--ks-patina);
}
.home-kinpaku .why-visual--v2 .v2-new-browser-frame {
background:
linear-gradient(90deg, oklch(78% 0.12 82 / 0.035) 1px, transparent 1px),
@@ -2385,10 +2445,12 @@
}
.home-kinpaku .live-demo-frame {
border: 1px solid var(--ks-rule);
border: 1px solid oklch(92% 0 0 / 0.1);
border-radius: 4px;
background: var(--ks-lacquer-raised);
box-shadow: 0 30px 84px oklch(2% 0.004 95 / 0.42);
box-shadow:
inset 0 1px 0 oklch(100% 0 0 / 0.05),
0 20px 48px -14px oklch(0% 0 0 / 0.6);
}
/* Picker chrome (chrome bar, gbar, ctx, outline, comment, stroke, etc.)
+107 -93
View File
@@ -29,6 +29,9 @@
never tucks under the nav; on tall viewports the content still centers. */
padding: 113px 0 96px;
overflow: hidden;
/* Hairline divider into the testimonials below, matching the testimonial
card border color. */
border-bottom: 1px solid oklch(0.64 0 0 / 0.22);
}
/* Header sits on top of the hero image like the /design-system topbar:
@@ -55,10 +58,7 @@
position: absolute;
inset: 0;
z-index: 0;
/* Top scrim darkens the band behind the overlaid nav so the links stay
legible over the bright kintsugi gold, fading out before the hero copy. */
background:
linear-gradient(180deg, oklch(7% 0.006 95 / 0.9) 0%, oklch(7% 0.006 95 / 0.5) 8%, transparent 18%),
url("/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png") center / cover no-repeat;
filter: saturate(1.2) contrast(1.08);
}
@@ -116,7 +116,9 @@
/* Hero CTAs migrated to .ks-button.ks-button-primary / .ks-button-secondary
(see kinpaku-kit.css). The .hero-rebuild-arrow class is gone too the
arrow now lives in .ks-button-arrow inside the button. */
arrow now lives in .ks-button-arrow inside the button.
"How it works" is the kit ghost variant: plain champagne (white) text, no
border or fill, hovering to gold. */
.hero-rebuild-meta {
margin: 24px 0 0;
@@ -497,13 +499,13 @@
than .home-kinpaku code alone). Keeps the CLI block in the softer code-gold
so the line reads as a terminal command, not a pumpkin headline. */
.home-kinpaku code.slop-teaser-cli {
color: oklch(82% 0.11 82);
color: var(--ks-code-block-fg);
font-family: var(--ks-mono);
font-size: 0.86rem;
background: var(--ks-lacquer-raised);
background: var(--ks-code-block-bg);
padding: 10px 16px;
border: 1px solid var(--ks-rule);
border-radius: 2px;
border: 1px solid var(--ks-code-block-border);
border-radius: var(--ks-code-block-radius);
user-select: all;
}
@@ -553,8 +555,8 @@
--card-fg-strong: var(--ks-champagne);
--card-fg-muted: var(--ks-muted);
--card-accent: var(--ks-kinpaku);
--card-cmd-bg: oklch(0.11 0 0);
--card-cmd-fg: var(--ks-champagne);
--card-cmd-bg: var(--ks-code-block-bg);
--card-cmd-fg: var(--ks-code-block-fg);
--card-copy-border: var(--ks-rule);
--card-copy-fg: var(--ks-kinpaku);
--card-copy-hover-bg: oklch(77% 0.14 82 / 0.08);
@@ -562,6 +564,14 @@
--card-link-underline: var(--ks-kinpaku);
--card-link-secondary: var(--ks-muted);
/* Command-frame accents. The install box is the primary CTA and carries
kinpaku; the update box carries patina, which in this system reads as the
"improved / updated" state. Both re-theme in light-mode.css. */
--card-cmd-border: var(--ks-kinpaku);
--card-prompt: var(--ks-kinpaku);
--card-cmd-border-update: oklch(70% 0.12 188 / 0.5);
--card-prompt-update: var(--ks-patina);
/* Width of the accent's reserved right column. Content padding-right
matches this so the accent never overlaps text. */
--card-accent-w: 280px;
@@ -569,11 +579,11 @@
position: relative;
max-width: 1080px;
margin: 0 auto;
padding: 88px var(--card-accent-w) 88px 56px;
padding: 80px var(--card-accent-w) 80px 56px;
border-radius: 2px;
background: oklch(0.17 0 0);
color: var(--card-fg);
text-align: center;
text-align: left;
overflow: hidden;
}
@@ -618,9 +628,9 @@
display: grid;
gap: 18px;
max-width: 760px;
margin: 0 auto 48px;
text-align: center;
justify-items: center;
margin: 0 0 40px;
text-align: left;
justify-items: start;
}
.downloads-rebuild-eyebrow {
@@ -643,24 +653,56 @@
.downloads-rebuild-install {
display: grid;
gap: 12px;
margin: 0 auto 56px;
max-width: 720px;
gap: 22px;
margin: 0 0 48px;
max-width: 600px;
text-align: left;
}
/* Install command is THE primary CTA of the Get Started section. Strong
plinth bg + bigger font + more padding make it the hero. Copy button is
nested inside on the right. No side-stripe border (brand-ban). */
/* Each command is a labeled group: a small mono cap above its box. */
.downloads-rebuild-cmd-group {
display: grid;
gap: 9px;
}
.downloads-rebuild-cmd-label {
font-family: var(--ks-mono);
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--card-prompt);
}
.downloads-rebuild-cmd-label--update {
color: var(--card-prompt-update);
}
/* Install command is THE primary CTA of the Get Started section. A full
kinpaku hairline frame + gold prompt glyph + left-aligned mono make it read
as a runnable terminal line, not a decorative chip. Copy button nests on the
right. Full border, not a side-stripe (brand-ban). */
.home-kinpaku .downloads-rebuild-cmd {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 14px 14px 28px;
gap: 14px;
padding: 15px 14px 15px 22px;
background: var(--card-cmd-bg);
border: 0;
border: 1px solid var(--card-cmd-border);
border-radius: 2px;
}
/* The shell prompt that signals "this is a command you run." Carries the
row's accent (gold for install, patina for update). */
.home-kinpaku .downloads-rebuild-prompt {
flex: none;
font-family: var(--ks-mono);
font-size: 1.18rem;
font-weight: 500;
line-height: 1;
color: var(--card-prompt);
user-select: none;
}
.home-kinpaku .downloads-rebuild-cmd > code {
flex: 1 1 auto;
min-width: 0;
@@ -668,7 +710,7 @@
white-space: nowrap;
color: var(--card-cmd-fg);
font-family: var(--ks-mono);
font-size: 1.25rem;
font-size: 1.22rem;
letter-spacing: 0;
background: transparent;
border: 0;
@@ -676,6 +718,14 @@
user-select: all;
}
/* Update box: same frame, quieter patina accent (the "improved" signal). */
.home-kinpaku .downloads-rebuild-cmd--update {
border-color: var(--card-cmd-border-update);
}
.home-kinpaku .downloads-rebuild-cmd--update .downloads-rebuild-prompt {
color: var(--card-prompt-update);
}
/* Compact icon-only copy button nested inside the cmd box, on the right. */
.home-kinpaku .downloads-rebuild-copy {
flex: none;
@@ -706,11 +756,20 @@
border-color: var(--card-copy-fg);
}
/* Update copy button tracks the patina accent rather than gold. */
.home-kinpaku .downloads-rebuild-cmd--update .downloads-rebuild-copy {
color: var(--card-prompt-update);
}
.home-kinpaku .downloads-rebuild-cmd--update .downloads-rebuild-copy:hover {
background: oklch(70% 0.12 188 / 0.08);
border-color: var(--card-prompt-update);
}
/* "Other install methods" a quiet disclosure under the primary command.
Native <details> so it works without JS and stays accessible. */
.downloads-rebuild-alts {
grid-column: 1 / -1;
margin: 2px 0 0;
text-align: left;
}
.downloads-rebuild-alts-summary {
display: inline-flex;
@@ -770,18 +829,25 @@
}
.downloads-rebuild-note {
grid-column: 1 / -1;
margin: 0;
max-width: 60ch;
text-align: left;
color: var(--card-fg-muted);
font-size: 0.88rem;
line-height: 1.55;
}
.home-kinpaku .downloads-rebuild-note code {
font-family: var(--ks-mono);
font-size: 0.86em;
color: var(--card-fg-strong);
}
/* Quieter than the note the changelog + FAQ hints sit just under the
primary "works with" line. Patina links so they read as related but
distinct from the kinpaku primary CTA above. */
.downloads-rebuild-secondary {
grid-column: 1 / -1;
margin: 6px 0 0;
text-align: left;
font-size: 0.84rem;
color: var(--ks-text-muted);
}
@@ -795,12 +861,10 @@
border-bottom-color: var(--ks-patina-pale);
}
/* Closing "go deeper" links under the usage examples point to the full
workflow page and the command docs. */
/* Closing links under the setup extras — workflow + command docs. */
.downloads-rebuild-more {
margin: 30px auto 0;
max-width: 720px;
text-align: center;
margin: 28px 0 0;
text-align: left;
font-size: 0.9rem;
color: var(--ks-text-muted);
}
@@ -814,70 +878,15 @@
border-bottom-color: var(--ks-patina-pale);
}
/* Center-aligned examples: each row is a vertical stack the command line
on top, the description below, both centered horizontally. */
/* Usage examples now sit after the extras (the logical next step once the
skill, extension, CLI, and subscriptions are set up), so the separating
gap moves to the top. */
.downloads-rebuild-examples {
list-style: none;
margin: 48px auto 0;
padding: 48px 0 0;
/* Subtle hairline marking the "now use it" step after the setup block. */
border-top: 1px solid var(--ks-rule);
display: grid;
gap: 22px;
max-width: 720px;
}
.downloads-rebuild-examples li {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
justify-items: center;
text-align: center;
}
/* Strip the global .home-kinpaku code chrome for these example rows. */
.home-kinpaku .downloads-rebuild-examples > li > code {
font-family: var(--ks-mono);
color: var(--card-accent);
font-size: 1rem;
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
letter-spacing: 0.005em;
}
.downloads-rebuild-slash {
opacity: 0.55;
}
.downloads-rebuild-examples span {
color: var(--card-fg);
font-size: 0.94rem;
line-height: 1.5;
}
.home-kinpaku .downloads-rebuild-examples span code {
color: var(--card-fg-strong);
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
font-family: var(--ks-mono);
font-size: 0.86rem;
}
.downloads-rebuild-extras {
list-style: none;
margin: 0 auto;
padding: 0;
margin: 0;
padding: 40px 0 0;
border-top: 1px solid var(--ks-rule);
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 36px;
max-width: 900px;
max-width: 820px;
}
/* Each cell is a flex column so primary links can be bottom-anchored.
@@ -950,7 +959,6 @@
@media (max-width: 720px) {
.downloads-rebuild { padding: 64px 0; }
.downloads-rebuild-install { grid-template-columns: 1fr; }
.downloads-rebuild-examples li { grid-template-columns: 1fr; gap: 8px; }
}
/* ============================================================================
@@ -1014,6 +1022,12 @@
max-width: 100%;
}
/* The hero-scoped demo-chrome refinements (crisp outline, neutral chrome dots /
URL, quiet gbar border, crisp active-Pick pill, tighter radii, refined frame
elevation) were promoted into the shared picker chrome in kinpaku-kit.css /
live-mode-kinpaku.css / home-kinpaku.css so /live-mode and the injected
picker (skill/scripts/live-browser.js) match. Nothing hero-specific remains. */
/* The stage holds a full (fake) website now, not a centered card. */
.hero-live-demo .live-demo-stage {
display: block;
+54 -21
View File
@@ -38,7 +38,7 @@
.ks-wordmark {
color: var(--ks-kinpaku);
font-family: var(--ks-font-wordmark);
font-weight: 500;
font-weight: 400;
font-size: 1.3rem;
letter-spacing: 0.15em;
text-transform: uppercase;
@@ -872,7 +872,7 @@
.kinpaku-chrome .footer-logo {
color: var(--ks-kinpaku);
font-family: var(--ks-font-wordmark);
font-weight: 500;
font-weight: 400;
font-size: 1.3rem;
letter-spacing: 0.15em;
text-transform: uppercase;
@@ -892,6 +892,17 @@
font-weight: 400;
}
.kinpaku-chrome .theme-toggle,
.kinpaku-chrome .site-header-github {
height: 36px;
min-height: 36px;
box-sizing: border-box;
}
.kinpaku-chrome .site-header-github {
padding: 0 12px 0 10px;
}
.kinpaku-chrome .site-header-nav a,
.kinpaku-chrome .site-header-github {
color: var(--ks-champagne);
@@ -904,7 +915,7 @@
/* GitHub star pill. Previously home-only; now every kinpaku page gets it. */
.kinpaku-chrome .site-header-github {
border: 1px solid var(--ks-rule);
border: 1px solid oklch(80% 0 0);
border-radius: 999px;
background: oklch(10% 0.006 95 / 0.74);
}
@@ -1099,16 +1110,21 @@
constants are kept in sync with these tokens by hand.
============================================================ */
/* Browser-chrome bar at the top of the mock (URL pill + dots). */
/* Browser-chrome bar at the top of the mock (URL pill + dots). Refined to read
like a precise tool, not a stock screenshot frame: slimmer bar, small uniform
neutral dots, and a clean neutral-edged URL pill (no warm gold hairline). */
.home-kinpaku .live-demo-chrome,
.live-mode-kinpaku .live-demo-chrome {
padding: 9px 14px;
background: var(--ks-graphite);
border-bottom: 1px solid var(--ks-rule);
border-bottom: 1px solid oklch(92% 0 0 / 0.07);
}
.home-kinpaku .live-demo-dot,
.live-mode-kinpaku .live-demo-dot {
background: oklch(28% 0.01 95);
width: 8px;
height: 8px;
background: oklch(34% 0 0);
}
.home-kinpaku .live-demo-url,
@@ -1119,7 +1135,7 @@
color: var(--ks-text-muted);
letter-spacing: 0.04em;
background: transparent;
border: 1px solid var(--ks-rule);
border: 1px solid oklch(92% 0 0 / 0.08);
}
/* Active control surfaces the bottom picker bar and the context bar
@@ -1131,10 +1147,12 @@
.home-kinpaku .live-demo-ctx,
.live-mode-kinpaku .live-demo-ctx {
background: var(--ks-lacquer-deep);
border: 1.5px solid var(--ks-kinpaku);
box-shadow:
0 0 0 1px oklch(78% 0.12 82 / 0.18),
0 10px 28px oklch(0% 0 0 / 0.28);
/* Quiet neutral hairline instead of the loud 1.5px gold border + gold halo.
Gold is reserved for the brand mark and the active control, not the
container outline. */
border: 1px solid oklch(92% 0 0 / 0.13);
border-radius: 8px;
box-shadow: 0 16px 36px -12px oklch(0% 0 0 / 0.6);
color: var(--ks-champagne);
}
@@ -1150,13 +1168,14 @@
background: transparent;
}
/* Active state soft kinpaku-dim pill with kinpaku text. The outer
gbar border carries the loud signal; the active button is the
quieter "currently selected" pill inside. */
/* Active state a crisp neutral pill with kinpaku text. The translucent
kinpaku-dim wash read as a murky smudge; a solid graphite chip with gold
text/icon makes the selected control precise. */
.home-kinpaku .live-demo-gbar-btn.is-active,
.live-mode-kinpaku .live-demo-gbar-btn.is-active {
background: oklch(78% 0.12 82 / 0.18);
background: oklch(27% 0 0);
color: var(--ks-kinpaku);
border-radius: 6px;
}
/* Page chat — collapsed pill with chat + mic; expands on click. */
@@ -1167,9 +1186,9 @@
height: 28px;
width: 88px;
margin: 0 4px 0 6px;
border-radius: 7px;
border-radius: 5px;
background: var(--ks-graphite);
border: 1px solid var(--ks-rule);
border: 1px solid oklch(92% 0 0 / 0.1);
overflow: hidden;
cursor: pointer;
flex-shrink: 0;
@@ -1292,10 +1311,20 @@
.home-kinpaku .live-demo-ctx-pill,
.live-mode-kinpaku .live-demo-ctx-pill {
background: var(--ks-graphite-2);
border: 1px solid var(--ks-rule);
border: 1px solid oklch(92% 0 0 / 0.12);
color: var(--ks-champagne);
}
/* Even heights across the configure row: the pill, input, count, and Go button
share one 30px baseline instead of stepping up and down. */
.home-kinpaku .live-demo-ctx-row--configure > *,
.live-mode-kinpaku .live-demo-ctx-row--configure > * {
height: 30px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
}
.home-kinpaku .live-demo-ctx-pill-caret,
.live-mode-kinpaku .live-demo-ctx-pill-caret {
color: var(--ks-text-muted);
@@ -1304,7 +1333,7 @@
.home-kinpaku .live-demo-ctx-input,
.live-mode-kinpaku .live-demo-ctx-input {
background: var(--ks-graphite);
border: 1px solid var(--ks-rule);
border: 1px solid oklch(92% 0 0 / 0.12);
color: var(--ks-champagne);
}
@@ -1316,7 +1345,7 @@
.home-kinpaku .live-demo-ctx-count,
.live-mode-kinpaku .live-demo-ctx-count {
background: var(--ks-graphite-2);
border: 1px solid var(--ks-rule);
border: 1px solid oklch(92% 0 0 / 0.12);
color: var(--ks-text-muted);
}
@@ -1377,8 +1406,12 @@
.home-kinpaku .live-demo-outline,
.live-mode-kinpaku .live-demo-outline {
border-color: var(--ks-kinpaku);
border-width: 1.5px;
border-radius: 3px;
color: var(--ks-kinpaku);
box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.16);
/* No soft gold glow ring (off-brand accent halo); a crisp outline reads as a
precise selection, not a highlighter. */
box-shadow: none;
}
.home-kinpaku .live-demo-annotations,
+93 -10
View File
@@ -15,6 +15,7 @@
*/
:root {
/* color-scheme follows html.light / html.dark class on <html> */
/* ============================================================
Brand anchors. Two anchors carry the brand kinpaku gold for
commitment and brand voice, verdigris patina for state and
@@ -45,22 +46,43 @@
--ks-graphite-2: oklch(19% 0.008 95); /* one step up from graphite */
/* ============================================================
Text. Warm champagne family. Body text reads at 0.92rem on
lacquer; smaller than that loses to the warm noise floor.
Text. The whole text ramp runs near-neutral and bright so copy,
headings, and labels read crisp on lacquer instead of mushing
into the warm floor. Warmth now lives only in the gold accents
and the surfaces, not in the type. (--ks-champagne keeps its name
for compatibility but is no longer warm.)
============================================================ */
--ks-champagne: oklch(84% 0.035 82); /* headlines, <strong> */
--ks-text: oklch(81% 0.03 82); /* body */
--ks-text-muted: oklch(63% 0.024 82); /* captions, meta */
--ks-text-faint: oklch(52% 0.018 82); /* subdued */
--ks-text-mute-deep: oklch(48% 0.018 82); /* disabled */
--ks-champagne: oklch(91% 0 0); /* headlines, <strong> — neutral white */
--ks-text: oklch(88% 0 0); /* body — neutral near-white */
--ks-text-muted: oklch(72% 0 0); /* captions, meta */
--ks-text-faint: oklch(62% 0 0); /* subdued */
--ks-text-mute-deep: oklch(52% 0 0); /* disabled */
/* ============================================================
Rules. Gold hairlines. Default is quiet; "strong" appears on
focus, active state, and structural anchors.
Rules. The default hairline is neutral so borders/dividers/labels
don't carry warmth (warmth lives in the gold accents and surfaces).
"strong" stays gold it appears on focus, active state, and
structural brand anchors where the gold is the signal.
============================================================ */
--ks-rule: oklch(58% 0.065 82 / 0.32);
--ks-rule: oklch(78% 0 0 / 0.16);
--ks-rule-strong: oklch(74% 0.09 82 / 0.6);
/* ============================================================
Code. One treatment site-wide. Inline code is a neutral graphite
chip; blocks / CLI commands are a deep lacquer terminal with a
neutral hairline. Gold is only for code that is itself a command
link (--ks-code-cmd). All code rules read these don't hand-type.
============================================================ */
--ks-code-fg: oklch(91% 0 0); /* inline code text */
--ks-code-bg: var(--ks-graphite-2); /* inline code chip */
--ks-code-radius: 3px;
--ks-code-pad: 0.3em 0.5em; /* inline code chip padding */
--ks-code-block-fg: oklch(86% 0 0); /* block / CLI text */
--ks-code-block-bg: var(--ks-lacquer-deep);
--ks-code-block-border: var(--ks-rule);
--ks-code-block-radius: 2px;
--ks-code-cmd: var(--ks-kinpaku); /* code that's a command link */
/* ============================================================
Typography three faces.
- --ks-font-display: Alumni Sans Pinstripe. Pinstripe horizontal
@@ -124,3 +146,64 @@
============================================================ */
--ks-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}
html.dark {
color-scheme: dark;
}
/* ============================================================
Light mode warm paper surfaces, dark ink type, kinpaku gold
accents deepened for contrast. Activated via html.light on
<html> (set by theme.js + inline FOUC guard in Base.astro).
============================================================ */
html.light {
color-scheme: light;
/* Brand gold same fills as dark mode (logo, buttons, marks).
Use --ks-kinpaku-ink for small text on paper where bright gold
lacks contrast; never swap the core token to a muddy orange. */
--ks-kinpaku: oklch(84% 0.19 80.46);
--ks-kinpaku-pale: oklch(86% 0.07 84);
--ks-kinpaku-rich: oklch(77% 0.13 82);
--ks-kinpaku-deep: oklch(61% 0.085 78);
--ks-kinpaku-ink: oklch(56% 0.11 82);
/* Text links + active nav on paper — patina, not fill gold. */
--ks-link-on-paper: var(--ks-patina-deep);
--ks-link-on-paper-hover: var(--ks-patina);
--ks-link-on-paper-line: oklch(49% 0.08 188 / 0.42);
--ks-nav-active: var(--ks-patina-deep);
/* Patina — brand values for fills; deep for text on paper. */
--ks-patina: oklch(70% 0.12 188);
--ks-patina-pale: oklch(82% 0.07 188);
--ks-patina-deep: oklch(49% 0.08 188);
--ks-vermilion: oklch(52% 0.16 35);
/* Surfaces — warm paper and cream panels. */
--ks-lacquer: oklch(97% 0.012 95);
--ks-lacquer-deep: oklch(94% 0.014 95);
--ks-lacquer-raised: oklch(99% 0.008 95);
--ks-graphite: oklch(91% 0.012 95);
--ks-graphite-2: oklch(88% 0.014 95);
/* Text — dark ink on paper. */
--ks-champagne: oklch(18% 0.02 95);
--ks-text: oklch(25% 0.018 95);
--ks-text-muted: oklch(45% 0.015 95);
--ks-text-faint: oklch(55% 0.012 95);
--ks-text-mute-deep: oklch(65% 0.01 95);
/* Rules — dark hairlines. */
--ks-rule: oklch(25% 0.02 95 / 0.12);
--ks-rule-strong: oklch(61% 0.085 78 / 0.45);
/* Code — light chips, dark text. */
--ks-code-fg: oklch(28% 0.02 95);
--ks-code-bg: var(--ks-graphite);
--ks-code-block-fg: oklch(30% 0.02 95);
--ks-code-block-bg: var(--ks-lacquer-raised);
--ks-code-block-border: var(--ks-rule);
--ks-code-cmd: var(--ks-kinpaku-ink);
}
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -190,10 +190,14 @@
.live-mode-kinpaku .live-demo-frame {
background: var(--ks-lacquer-deep);
border: 1px solid var(--ks-rule);
/* Crisp neutral hairline + tighter, deeper shadow that registers, instead of
a warm gold edge and a broad soft shadow that washes out on dark. */
border: 1px solid oklch(92% 0 0 / 0.1);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 32px 80px oklch(2% 0.004 95 / 0.55);
box-shadow:
inset 0 1px 0 oklch(100% 0 0 / 0.05),
0 20px 48px -14px oklch(0% 0 0 / 0.6);
}
/* All .live-demo-* picker chrome (chrome bar, gbar, ctx, outline,
+21 -44
View File
@@ -10,68 +10,43 @@
spans the full viewport. */
width: 100vw;
margin-left: calc(50% - 50vw);
padding: 20px 0 clamp(56px, 7vw, 96px);
background: var(--ks-lacquer);
/* No background of its own: the body's lacquer gradient is the page ground,
so the section sits on it directly and an opaque fill here would only
duplicate (and, when overlapped, mask) it. */
padding: 0 0 1em;
color: var(--ks-text);
border-top: 0;
overflow: hidden;
}
/* Flat dotted top divider (matches the hero → testimonials boundary). */
.home-kinpaku .testimonials-section::before {
/* Dotted accent at the bottom edge a deliberate separator between the
testimonials and the section below. Neutral dots, matching the de-warmed
borders. (This is the dot rail that used to sit above the section.) */
.home-kinpaku .testimonials-section::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 100vw;
height: 18px;
transform: translateX(-50%);
background-image: radial-gradient(circle, oklch(0.42 0.014 82) 0.9px, transparent 1.4px);
background-image: radial-gradient(circle, oklch(0.45 0 0) 0.9px, transparent 1.4px);
background-size: 7px 6px;
background-position: 0 1px;
background-repeat: repeat;
pointer-events: none;
}
/* ---------------------------------------------------------------------------
Plinth the tinted graphite shelf the marquee rides on
--------------------------------------------------------------------------- */
.home-kinpaku .t-plinth {
position: relative;
width: 100vw;
padding: 20px 0;
background:
linear-gradient(180deg, oklch(20% 0.01 78) 0%, oklch(17% 0.008 78) 100%);
border-top: 1px solid oklch(78% 0.12 82 / 0.12);
border-bottom: 1px solid oklch(78% 0.12 82 / 0.10);
}
.home-kinpaku .t-plinth--shelf::before,
.home-kinpaku .t-plinth--shelf::after {
content: "";
position: absolute;
left: 0; right: 0;
height: 8px;
pointer-events: none;
z-index: 1;
}
.home-kinpaku .t-plinth--shelf::before {
top: 0;
background: linear-gradient(180deg, oklch(0% 0 0 / 0.18), transparent);
}
.home-kinpaku .t-plinth--shelf::after {
bottom: 0;
background: linear-gradient(0deg, oklch(0% 0 0 / 0.22), transparent);
}
/* ---------------------------------------------------------------------------
Marquee engine
Marquee engine. The cards are outlined and ride directly on the section
ground the old graphite plinth wrapper was removed once it stopped being
a visible shelf, so the marquee carries its own vertical inset here.
--------------------------------------------------------------------------- */
.home-kinpaku .t-marquee {
display: grid;
gap: 14px;
width: 100%;
padding: 22px 0;
}
.home-kinpaku .t-marquee-row { overflow: hidden; }
@@ -119,14 +94,16 @@
transition: transform 200ms ease, background 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
}
/* Plinth-resident card — borderless, slightly raised tone */
/* Outlined card: no fill, just a solid neutral border on the page ground.
On dark, a clean outline separates more honestly than a near-invisible
fill or a dead drop-shadow ever did. */
.home-kinpaku .t-card--plinth {
background: oklch(13% 0.008 95);
border: 1px solid transparent;
box-shadow: 0 1px 0 0 oklch(78% 0.12 82 / 0.06) inset, 0 4px 12px -8px oklch(0% 0 0 / 0.6);
background: none;
border: 1px solid oklch(0.64 0 0 / 0.22);
box-shadow: none;
}
.home-kinpaku .t-card--plinth:hover {
background: oklch(15% 0.012 78);
border-color: oklch(0.64 0 0 / 0.42);
transform: translateY(-1px);
}
+16
View File
@@ -106,3 +106,19 @@ button, input, textarea, select {
--cat-system-border: #78716c;
--cat-system-text: #44403c;
}
/* Light mode legacy aliases — mirror html.light --ks-* tokens. */
html.light {
--color-ink: var(--ks-champagne);
--color-text: var(--ks-text);
--color-paper: var(--ks-lacquer);
--color-cream: var(--ks-lacquer-raised);
--color-charcoal: var(--ks-text);
--color-ash: var(--ks-text-muted);
--color-mist: var(--ks-rule);
--color-bg: var(--ks-lacquer-raised);
--color-accent: var(--ks-kinpaku);
--color-accent-hover: var(--ks-kinpaku-pale);
--color-accent-dim: oklch(77% 0.13 82 / 0.14);
--color-accent-soft: oklch(77% 0.13 82 / 0.24);
}
+24 -14
View File
@@ -46,9 +46,11 @@
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
@@ -995,8 +997,8 @@
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
background: BP.surface,
border: '1.5px solid ' + BP.border,
borderRadius: '10px',
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
@@ -1071,7 +1073,7 @@
barEl.innerHTML = '';
// Reset bar styling to the kinpaku picker palette
barEl.style.background = BP.surface;
barEl.style.border = '1.5px solid ' + BP.border;
barEl.style.border = '1px solid ' + BP.border;
barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
@@ -2339,8 +2341,8 @@
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
padding: '6px',
fontFamily: FONT,
@@ -2786,13 +2788,14 @@
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.style.webkitUserModify = 'read-write-plaintext-only';
row.el.addEventListener('input', onInlineInput);
}
}
@@ -2803,10 +2806,10 @@
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.style.webkitUserModify = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
@@ -6122,8 +6125,15 @@ void main() {
return {
surface: C.ink,
surfaceDeep: C.ink,
border: C.brand,
hairline: 'oklch(58% 0.065 82 / 0.48)',
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(84% 0.035 82)',
textDim: 'oklch(63% 0.024 82)',
accent: C.brand,
@@ -7032,8 +7042,8 @@ void main() {
display: 'flex', alignItems: 'stretch',
gap: '0',
background: P.surface,
border: '1.5px solid ' + P.border,
borderRadius: '10px',
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
@@ -7443,7 +7453,7 @@ void main() {
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.accentSoft : 'transparent';
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
+93 -258
View File
@@ -3,7 +3,7 @@
*
* Creates real temp directories, runs the CLI, and verifies results.
*
* Pure blocks (already-installed detection, prefix rename/round-trip) run in the
* Pure blocks (already-installed detection, unprefix migration) run in the
* default `bun run test`. Network blocks that download the universal bundle use
* `describeNet` and run only under `bun run test:cli-e2e` (IMPECCABLE_CLI_E2E=1),
* skipping gracefully when impeccable.style is unreachable.
@@ -13,6 +13,7 @@ import { execSync } from 'child_process';
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { migrateUnprefixImpeccable } from '../cli/bin/commands/skills.mjs';
const CLI = join(import.meta.dir, '..', 'cli', 'bin', 'cli.js');
@@ -43,6 +44,25 @@ function createFakeSkills(root, skills = ['audit', 'polish', 'impeccable'], prov
}
}
/** Write one fake skill dir with a SKILL.md naming itself. */
function writeSkill(root, provider, name) {
const dir = join(root, provider, 'skills', name);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'SKILL.md'), `---\nname: ${name}\n---\nRun /${name}.\n`);
}
/**
* Simulate an install from the era when the CLI offered a command prefix: the
* skill lives at `<prefix>impeccable`. Optionally drop in a third-party skill
* (one that even starts with the same prefix) that migration must NOT touch.
*/
function createPrefixedInstall(root, { prefix = 'i-', providers = ['.claude'], foreign = null } = {}) {
for (const provider of providers) {
writeSkill(root, provider, `${prefix}impeccable`);
if (foreign) writeSkill(root, provider, foreign);
}
}
// ─── Already-installed detection ─────────────────────────────────────────────
// Network e2e blocks (real bundle downloads from impeccable.style) run only
@@ -86,103 +106,79 @@ describe('skills install: already-installed detection', () => {
}, 15000);
});
// ─── Prefix rename (real filesystem) ─────────────────────────────────────────
// ─── Unprefix migration (real implementation, real filesystem) ───────────────
//
// The CLI no longer offers a command prefix (the `i-` rename only made sense
// when each command was its own skill). migrateUnprefixImpeccable retires any
// old `<prefix>impeccable` install back to the canonical `impeccable`, so an
// update lands fresh content there instead of orphaning the prefixed copy.
// These call the EXPORTED function -- not a reimplementation -- so a regression
// in the real code fails the suite.
describe('skills install: prefix rename', () => {
let tmp;
describe('skills: unprefix migration', () => {
test('renames i-impeccable back to impeccable across every provider', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-'));
createPrefixedInstall(tmp, { prefix: 'i-', providers: ['.claude', '.cursor'] });
beforeAll(() => {
tmp = mkdtempSync(join(tmpdir(), 'imp-test-pfx-'));
createFakeSkills(tmp, ['audit', 'polish', 'impeccable'], ['.claude', '.cursor']);
const migrated = migrateUnprefixImpeccable(tmp);
expect(migrated).toBe(2); // one skill x two providers
for (const provider of ['.claude', '.cursor']) {
const skills = readdirSync(join(tmp, provider, 'skills'));
expect(skills).toContain('impeccable');
expect(skills).not.toContain('i-impeccable');
}
rmSync(tmp, { recursive: true, force: true });
});
afterAll(() => {
if (tmp) rmSync(tmp, { recursive: true, force: true });
test('migrates a custom prefix too (x-impeccable)', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-x-'));
createPrefixedInstall(tmp, { prefix: 'x-' });
expect(migrateUnprefixImpeccable(tmp)).toBe(1);
expect(readdirSync(join(tmp, '.claude', 'skills'))).toContain('impeccable');
rmSync(tmp, { recursive: true, force: true });
});
test('renames folders with prefix', () => {
// Write a helper script that imports and runs renameSkillsWithPrefix
const helperScript = join(tmp, '_test_rename.mjs');
writeFileSync(helperScript, `
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
test('REGRESSION: never touches third-party skills, even ones starting with i-', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-scope-'));
// A foreign skill that shares the i- prefix but is NOT impeccable.
createPrefixedInstall(tmp, { prefix: 'i-', foreign: 'i-cool-skill' });
function escapeRegex(str) {
return str.replace(/[.*+?^$\{\}()|[\\]\\\\]/g, '\\\\$&');
}
const migrated = migrateUnprefixImpeccable(tmp);
expect(migrated).toBe(1); // only i-impeccable
function prefixSkillContent(content, prefix, allSkillNames) {
let result = content.replace(/^name:\\s*(.+)$/m, (_, name) => 'name: ' + prefix + name.trim());
const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
for (const name of sorted) {
result = result.replace(
new RegExp('/' + '(?=' + escapeRegex(name) + '(?:[^a-zA-Z0-9_-]|$))', 'g'),
'/' + prefix
);
result = result.replace(
new RegExp('(the) ' + escapeRegex(name) + ' skill', 'gi'),
(_, article) => article + ' ' + prefix + name + ' skill'
);
}
return result;
}
const DIRS = ['.claude', '.cursor'];
const root = process.argv[2];
const prefix = process.argv[3];
let allNames = [];
for (const d of DIRS) {
const dir = join(root, d, 'skills');
if (!existsSync(dir)) continue;
allNames = readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name);
if (allNames.length > 0) break;
}
let count = 0;
for (const d of DIRS) {
const dir = join(root, d, 'skills');
if (!existsSync(dir)) continue;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith(prefix)) continue;
const skillMd = join(dir, entry.name, 'SKILL.md');
if (!existsSync(skillMd)) continue;
renameSync(join(dir, entry.name), join(dir, prefix + entry.name));
let content = readFileSync(join(dir, prefix + entry.name, 'SKILL.md'), 'utf8');
content = prefixSkillContent(content, prefix, allNames);
writeFileSync(join(dir, prefix + entry.name, 'SKILL.md'), content);
count++;
}
}
console.log(JSON.stringify({ count }));
`);
const output = JSON.parse(execSync(`node ${helperScript} ${tmp} i-`, { encoding: 'utf8' }));
expect(output.count).toBe(6); // 3 skills x 2 providers
// Verify folders renamed
const skills = readdirSync(join(tmp, '.claude', 'skills'));
expect(skills).toContain('i-audit');
expect(skills).toContain('i-polish');
expect(skills).toContain('i-impeccable');
expect(skills).not.toContain('audit');
expect(skills).not.toContain('polish');
}, 15000);
expect(skills).toContain('impeccable');
expect(skills).toContain('i-cool-skill'); // untouched, NOT renamed to cool-skill
expect(skills).not.toContain('cool-skill');
test('prefixed SKILL.md has correct name and cross-references', () => {
const content = readFileSync(join(tmp, '.claude', 'skills', 'i-audit', 'SKILL.md'), 'utf8');
expect(content).toContain('name: i-audit');
expect(content).toContain('/i-audit');
expect(content).toContain('/i-polish');
expect(content).toContain('the i-impeccable skill');
// Original unprefixed references should be gone
expect(content).not.toMatch(/\/audit(?=[^a-zA-Z0-9_-]|$)/);
const foreign = readFileSync(join(tmp, '.claude', 'skills', 'i-cool-skill', 'SKILL.md'), 'utf8');
expect(foreign).toContain('name: i-cool-skill');
rmSync(tmp, { recursive: true, force: true });
});
test('also prefixed in second provider', () => {
const skills = readdirSync(join(tmp, '.cursor', 'skills'));
expect(skills).toContain('i-audit');
expect(skills).toContain('i-impeccable');
test('leaves a clean impeccable install alone (no-op)', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-clean-'));
createFakeSkills(tmp, ['impeccable'], ['.claude']);
expect(migrateUnprefixImpeccable(tmp)).toBe(0);
expect(readdirSync(join(tmp, '.claude', 'skills'))).toContain('impeccable');
rmSync(tmp, { recursive: true, force: true });
});
test('leaves the legacy teach-impeccable name alone (cleanup owns that)', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-legacy-'));
createFakeSkills(tmp, ['teach-impeccable'], ['.claude']);
expect(migrateUnprefixImpeccable(tmp)).toBe(0);
expect(readdirSync(join(tmp, '.claude', 'skills'))).toContain('teach-impeccable');
rmSync(tmp, { recursive: true, force: true });
});
});
@@ -223,162 +219,6 @@ describeNet('skills update: refreshes from the universal bundle', () => {
});
});
// ─── Prefix round-trip: detect, undo, re-apply ──────────────────────────────
describe('prefix round-trip: detect, undo, re-apply', () => {
let tmp;
beforeAll(() => {
tmp = mkdtempSync(join(tmpdir(), 'imp-test-roundtrip-'));
// Create prefixed skills to simulate post-install state
for (const skill of ['audit', 'polish', 'impeccable']) {
const skillDir = join(tmp, '.claude', 'skills', 'i-' + skill);
mkdirSync(skillDir, { recursive: true });
writeFileSync(join(skillDir, 'SKILL.md'), [
'---',
`name: i-${skill}`,
'user-invocable: true',
'---',
'',
'Run /i-audit first, then /i-polish to finish.',
'Use the i-impeccable skill for setup.',
].join('\n'));
}
});
afterAll(() => {
if (tmp) rmSync(tmp, { recursive: true, force: true });
});
test('detectPrefix finds the prefix from skill names', () => {
const script = join(tmp, '_detect.mjs');
writeFileSync(script, `
import { existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
const DIRS = ['.claude', '.cursor', '.agents'];
const root = ${JSON.stringify(tmp)};
for (const d of DIRS) {
const dir = join(root, d, 'skills');
if (!existsSync(dir)) continue;
for (const name of readdirSync(dir)) {
if (name === 'impeccable') { console.log(''); process.exit(); }
if (name.endsWith('-impeccable')) { console.log(name.slice(0, -'impeccable'.length)); process.exit(); }
}
}
console.log('');
`);
const output = execSync(`node ${script}`, { encoding: 'utf8' }).trim();
expect(output).toBe('i-');
});
test('undo removes prefix from folders and content', () => {
// Write undo helper script
const script = join(tmp, '_undo.mjs');
writeFileSync(script, `
import { existsSync, readdirSync, readFileSync, lstatSync, readlinkSync, unlinkSync, renameSync, writeFileSync, symlinkSync } from 'node:fs';
import { join } from 'node:path';
function escapeRegex(str) { return str.replace(/[.*+?^$\{\\}()|[\\]\\\\]/g, '\\\\$&'); }
const root = ${JSON.stringify(tmp)};
const prefix = 'i-';
const skillsDir = join(root, '.claude', 'skills');
const entries = readdirSync(skillsDir);
const prefixedNames = entries.filter(n => n.startsWith(prefix));
for (const name of entries) {
if (!name.startsWith(prefix)) continue;
const unprefixed = name.slice(prefix.length);
const src = join(skillsDir, name);
const dest = join(skillsDir, unprefixed);
if (lstatSync(src).isSymbolicLink()) {
const target = readlinkSync(src);
unlinkSync(src);
symlinkSync(target.replace('/' + name, '/' + unprefixed), dest);
} else {
renameSync(src, dest);
const skillMd = join(dest, 'SKILL.md');
if (existsSync(skillMd)) {
let content = readFileSync(skillMd, 'utf8');
content = content.replace(new RegExp('^name:\\\\s*' + escapeRegex(prefix), 'm'), 'name: ');
const sorted = [...prefixedNames].sort((a, b) => b.length - a.length);
for (const pName of sorted) {
const uName = pName.slice(prefix.length);
content = content.replace(new RegExp('/' + escapeRegex(pName) + '(?=[^a-zA-Z0-9_-]|$)', 'g'), '/' + uName);
content = content.replace(new RegExp('(the) ' + escapeRegex(pName) + ' skill', 'gi'), '$1 ' + uName + ' skill');
}
writeFileSync(skillMd, content);
}
}
}
console.log(JSON.stringify(readdirSync(skillsDir)));
`);
const output = JSON.parse(execSync(`node ${script}`, { encoding: 'utf8' }));
expect(output).toContain('audit');
expect(output).toContain('polish');
expect(output).toContain('impeccable');
expect(output).not.toContain('i-audit');
// Verify content was un-prefixed
const content = readFileSync(join(tmp, '.claude', 'skills', 'audit', 'SKILL.md'), 'utf8');
expect(content).toContain('name: audit');
expect(content).toContain('/audit');
expect(content).toContain('/polish');
expect(content).toContain('the impeccable skill');
expect(content).not.toContain('/i-audit');
expect(content).not.toContain('i-impeccable');
});
test('re-applying prefix restores original state', () => {
// Now re-apply using the same helper from the rename test
const script = join(tmp, '_reprefix.mjs');
writeFileSync(script, `
import { existsSync, readdirSync, readFileSync, statSync, lstatSync, renameSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
function escapeRegex(str) { return str.replace(/[.*+?^$\{\\}()|[\\]\\\\]/g, '\\\\$&'); }
const root = ${JSON.stringify(tmp)};
const prefix = 'i-';
const skillsDir = join(root, '.claude', 'skills');
const allNames = readdirSync(skillsDir).filter(n => {
const full = join(skillsDir, n);
try { return statSync(full).isDirectory() && existsSync(join(full, 'SKILL.md')); } catch { return false; }
});
for (const name of allNames) {
if (name.startsWith(prefix)) continue;
const src = join(skillsDir, name);
const dest = join(skillsDir, prefix + name);
const ls = lstatSync(src);
if (ls.isSymbolicLink() || !ls.isDirectory()) continue;
renameSync(src, dest);
let content = readFileSync(join(dest, 'SKILL.md'), 'utf8');
content = content.replace(/^name:\\s*(.+)$/m, (_, n) => 'name: ' + prefix + n.trim());
const sorted = [...allNames].sort((a, b) => b.length - a.length);
for (const n of sorted) {
content = content.replace(new RegExp('/' + '(?=' + escapeRegex(n) + '(?:[^a-zA-Z0-9_-]|$))', 'g'), '/' + prefix);
content = content.replace(new RegExp('(the) ' + escapeRegex(n) + ' skill', 'gi'), (_, art) => art + ' ' + prefix + n + ' skill');
}
writeFileSync(join(dest, 'SKILL.md'), content);
}
console.log(JSON.stringify(readdirSync(skillsDir)));
`);
const output = JSON.parse(execSync(`node ${script}`, { encoding: 'utf8' }));
expect(output).toContain('i-audit');
expect(output).toContain('i-polish');
expect(output).toContain('i-impeccable');
expect(output).not.toContain('audit');
// Verify content was re-prefixed
const content = readFileSync(join(tmp, '.claude', 'skills', 'i-audit', 'SKILL.md'), 'utf8');
expect(content).toContain('name: i-audit');
expect(content).toContain('/i-polish');
expect(content).toContain('the i-impeccable skill');
});
});
// ─── Full install e2e (downloads the universal bundle) ───────────────────────
describeNet('skills install: full e2e (universal bundle download)', () => {
@@ -404,22 +244,17 @@ describeNet('skills install: full e2e (universal bundle download)', () => {
expect(hasSkills).toBe(true);
}, 90000);
test('install with --prefix= renames all skills', () => {
const output = run('skills install -y --force --prefix=x-', { cwd: tmp });
test('--force reinstall over an old prefixed install lands on canonical impeccable', () => {
// Seed a stale prefixed install, then reinstall. The migration should
// retire i-impeccable so we are left with the canonical name only.
const prefixed = join(tmp, '.claude', 'skills', 'i-impeccable');
mkdirSync(prefixed, { recursive: true });
writeFileSync(join(prefixed, 'SKILL.md'), '---\nname: i-impeccable\n---\n');
// Find the provider that has skills
let found = false;
for (const d of ['.claude', '.cursor', '.gemini', '.agents', '.kiro']) {
const dir = join(tmp, d, 'skills');
if (!existsSync(dir)) continue;
const skills = readdirSync(dir);
if (skills.length === 0) continue;
found = true;
const prefixed = skills.filter(s => s.startsWith('x-'));
// All skills should be prefixed
expect(prefixed.length).toBe(skills.length);
break;
}
expect(found).toBe(true);
run('skills install -y --force', { cwd: tmp });
const skills = readdirSync(join(tmp, '.claude', 'skills'));
expect(skills).toContain('impeccable');
expect(skills).not.toContain('i-impeccable');
}, 90000);
});