diff --git a/package.json b/package.json index a09f2ae30..fa8e324f6 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,8 @@ "build:picker": "node scripts/build-picker.mjs", "build:skills": "bun run scripts/build.js --skip-root-sync", "build:skills:release": "bun run scripts/build.js", - "build:site": "npx astro build", - "build": "bun run build:picker && bun run build:skills && bun run build:site && cp -R dist build/_data/dist", - "build:release": "bun run build:picker && bun run build:skills:release && bun run build:site && cp -R dist build/_data/dist", + "build": "bun run build:picker && bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist", + "build:release": "bun run build:picker && bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist", "build:browser": "node scripts/build-browser-detector.js", "build:extension": "node scripts/build-extension.js", "clean": "rm -rf dist build", @@ -95,6 +94,7 @@ "@babel/parser": "^8.0.4", "ai": "^7.0.14", "archiver": "^8.0.0", + "astro": "^7.0.0", "playwright": "^1.59.1", "svelte": "^5", "zod": "^4.3.6" diff --git a/picker/assets/favicon.svg b/picker/assets/favicon.svg new file mode 100644 index 000000000..a6c6d2979 --- /dev/null +++ b/picker/assets/favicon.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/picker/assets/hero-light.jpg b/picker/assets/hero-light.jpg new file mode 100644 index 000000000..d3837d59e Binary files /dev/null and b/picker/assets/hero-light.jpg differ diff --git a/picker/styles/picker.css b/picker/styles/picker.css index c732a6c49..a92b3360a 100644 --- a/picker/styles/picker.css +++ b/picker/styles/picker.css @@ -1,5 +1,7 @@ -@import "../../site/styles/kinpaku-tokens.css"; -@import "../../site/styles/kinpaku-kit.css"; +/* Vendored from the site's design system (now in the private impeccable-site + repo) so the picker stays self-contained. Update by re-copying from there. */ +@import "./vendor/kinpaku-tokens.css"; +@import "./vendor/kinpaku-kit.css"; /* Scoped subset of the reset imported by site/styles/main.css through site/styles/tokens.css (lines 12-27). */ @@ -170,10 +172,12 @@ html.light .ks-button.ks-button-primary:active { .picker-progress span { display: block; - width: calc(var(--progress, 0) * 100%); + width: 100%; height: 100%; background: var(--ks-champagne); - transition: width 180ms var(--ks-ease); + transform: scaleX(var(--progress, 0)); + transform-origin: left; + transition: transform 180ms var(--ks-ease); } #picker-form[data-current="01"] .picker-progress { diff --git a/picker/styles/vendor/kinpaku-kit.css b/picker/styles/vendor/kinpaku-kit.css new file mode 100644 index 000000000..a65db442a --- /dev/null +++ b/picker/styles/vendor/kinpaku-kit.css @@ -0,0 +1,1638 @@ +/* ============================================================================ + Impeccable — Neo Kinpaku kit (page-global primitives) + + Reusable component primitives lifted from /design-system so they work on + every page. Brand lockup, section scaffolding, buttons, form controls, + tabs, badges, tags, pagination, toasts, tooltips, modal, empty state, + changelog, skeleton. No `.kinpaku-system-page` scope — drop these classes + onto any element on any page. + + All values resolve through kinpaku-tokens.css. If a token name changes + there, this file follows. + + Imported globally from Base.astro so every page picks them up. + ============================================================================ */ + + +/* ============================================================ + Brand lockup — mark + wordmark + ============================================================ */ + +.ks-brand { + display: inline-flex; + align-items: center; + gap: 4px; + text-decoration: none; +} + +.ks-mark { + width: 38px; + height: 38px; + display: grid; + place-items: center; + color: var(--ks-kinpaku); +} + +.ks-mark svg { width: 32px; height: 32px; } + +.ks-wordmark { + color: var(--ks-kinpaku); + font-family: var(--ks-font-wordmark); + font-weight: 400; + font-size: 1.3rem; + letter-spacing: 0.15em; + text-transform: uppercase; + line-height: 1; + -webkit-font-smoothing: auto; +} + + +/* ============================================================ + Section scaffolding — page-level sections, eyebrows, subsections + ============================================================ */ + +.ks-section { + max-width: 1320px; + margin: 0 auto; + padding: 110px 56px; +} + +.ks-section + .ks-section { + border-top: 1px solid var(--ks-rule); +} + +.ks-section-head { + display: grid; + gap: 14px; + margin-bottom: 56px; + max-width: 720px; +} + +.ks-section-eyebrow { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.28em; + text-transform: uppercase; + margin: 0; +} + +.ks-section-head h2 { + margin: 0; + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-size: clamp(2.2rem, 3.6vw, 3rem); + font-weight: var(--ks-type-headline-weight); + line-height: 1.06; + letter-spacing: -0.005em; +} + +.ks-subsection { + margin-top: 56px; +} + +.ks-subsection-label { + display: block; + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; + margin-bottom: 22px; +} + +.ks-section-sub { + color: var(--ks-text-muted); + font-size: 0.96rem; + line-height: 1.55; + margin: 4px 0 0; + max-width: 60ch; +} + + +/* ============================================================ + Buttons — primary, secondary, ghost, disabled + ============================================================ */ + +.ks-button { + --b-shift: 0; + position: relative; + min-height: 52px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 14px; + padding: 0 28px; + border-radius: 2px; + font-size: 0.96rem; + font-weight: 500; + line-height: 1; + text-decoration: none; + border: 1px solid transparent; + cursor: pointer; + transition: transform 180ms var(--ks-ease), background-color 180ms var(--ks-ease), border-color 180ms var(--ks-ease), color 180ms var(--ks-ease); +} + +.ks-button .ks-button-arrow { + width: 16px; + height: 8px; + display: inline-block; + flex: none; +} + +.ks-button-arrow svg { width: 100%; height: 100%; display: block; } + +/* Warm-gold flat primary. Hover lifts to pale; active darkens to rich. + Variants chain on .ks-button so specificity (0,2,0) wins over generic page + anchor resets like `.kinpaku-system-page a { color: inherit }` (0,1,1). + Use class="ks-button ks-button-primary" — both classes required. */ +.ks-button.ks-button-primary { + color: var(--ks-lacquer-deep); + background: var(--ks-kinpaku); + border-color: var(--ks-kinpaku); +} + +.ks-button.ks-button-primary:hover { + background: var(--ks-kinpaku-pale); + border-color: var(--ks-kinpaku-pale); + color: var(--ks-lacquer-deep); + transform: translateY(-1px); +} + +.ks-button.ks-button-primary:active { + transform: translateY(0); + background: var(--ks-kinpaku-rich); + border-color: var(--ks-kinpaku-rich); +} + +.ks-button.ks-button-secondary { + color: var(--ks-kinpaku); + background: transparent; + border-color: var(--ks-kinpaku); +} + +.ks-button.ks-button-secondary:hover { + background: oklch(77% 0.14 82 / 0.08); + color: var(--ks-kinpaku); + transform: translateY(-1px); +} + +.ks-button.ks-button-secondary:active { + background: oklch(77% 0.14 82 / 0.16); + transform: translateY(0); +} + +.ks-button.ks-button-ghost { + color: var(--ks-champagne); + background: transparent; + border-color: transparent; + padding: 0 14px; +} + +.ks-button.ks-button-ghost:hover { + color: var(--ks-kinpaku); +} + +.ks-button[disabled], +.ks-button.ks-button-disabled { + color: var(--ks-text-mute-deep); + background: transparent; + border-color: var(--ks-rule); + cursor: not-allowed; + transform: none !important; +} + +.ks-button:focus-visible { + outline: 2px solid var(--ks-patina); + outline-offset: 3px; +} + +.ks-button-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 18px; +} + + +/* ============================================================ + Form controls — input, toggle, checkbox, select + ============================================================ */ + +.ks-form-sample { + display: grid; + gap: 22px; + max-width: 360px; +} + +.ks-form-sample label { + display: grid; + gap: 8px; + color: var(--ks-text-muted); + font-size: 0.82rem; + letter-spacing: 0.04em; +} + +.ks-form-sample input[type="search"], +.ks-form-sample input[type="text"] { + min-height: 46px; + padding: 0 14px; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background: var(--ks-lacquer-raised); + color: var(--ks-champagne); + font-size: 0.92rem; +} + +.ks-form-sample input[type="search"]:focus, +.ks-form-sample input[type="text"]:focus { + outline: none; + border-color: var(--ks-patina); +} + +.ks-toggle { + display: flex !important; + align-items: center; + gap: 12px !important; +} + +.ks-toggle input { + appearance: none; + width: 44px; + height: 24px; + border-radius: 999px; + border: 1px solid var(--ks-rule); + background: var(--ks-graphite); + position: relative; + cursor: pointer; + flex: none; +} + +.ks-toggle input::before { + content: ""; + position: absolute; + width: 16px; + height: 16px; + left: 3px; + top: 3px; + border-radius: 999px; + background: var(--ks-text-muted); + transition: transform 220ms var(--ks-ease), background 220ms var(--ks-ease); +} + +.ks-toggle input:checked { + border-color: var(--ks-patina); + background: oklch(48% 0.08 188 / 0.2); +} + +.ks-toggle input:checked::before { + transform: translateX(20px); + background: var(--ks-patina); +} + +.ks-toggle span { color: var(--ks-champagne); font-size: 0.92rem; } + +.ks-checkbox { + display: flex; + align-items: center; + gap: 10px; + color: var(--ks-champagne); + font-size: 0.92rem; + cursor: pointer; +} + +.ks-checkbox input { + appearance: none; + width: 18px; + height: 18px; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background: var(--ks-lacquer-raised); + position: relative; + cursor: pointer; + flex: none; +} + +.ks-checkbox input:checked { + border-color: var(--ks-kinpaku); + background: var(--ks-kinpaku); +} + +.ks-checkbox input:checked::after { + content: ""; + position: absolute; + left: 5px; + top: 2px; + width: 5px; + height: 9px; + border: solid var(--ks-lacquer-deep); + border-width: 0 1.5px 1.5px 0; + transform: rotate(45deg); +} + +.ks-select { + appearance: none; + min-height: 46px; + padding: 0 38px 0 14px; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background-color: var(--ks-lacquer-raised); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6' fill='none' stroke='%23c89a5a' stroke-width='1.2'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 14px center; + background-size: 10px 6px; + color: var(--ks-champagne); + font-size: 0.92rem; + cursor: pointer; +} + + +/* ============================================================ + Tabs + ============================================================ */ + +.ks-tabs { max-width: 460px; } + +.ks-tab-list { display: flex; border-bottom: 1px solid var(--ks-rule); } + +.ks-tab-list button { + flex: 1; + min-height: 42px; + border: 0; + border-bottom: 2px solid transparent; + background: transparent; + color: var(--ks-text-muted); + cursor: pointer; + font-size: 0.92rem; + transition: color 180ms var(--ks-ease), border-color 180ms var(--ks-ease); +} + +.ks-tab-list button:hover { color: var(--ks-champagne); } + +.ks-tab-list button[aria-selected="true"] { + color: var(--ks-kinpaku); + border-bottom-color: var(--ks-kinpaku); +} + +.ks-tab-panel { + padding: 22px 4px 0; + color: var(--ks-champagne); + font-size: 0.92rem; + line-height: 1.6; +} + + +/* ============================================================ + Status badges + issue tags + ============================================================ */ + +.ks-badge-row { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.ks-badge { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 30px; + padding: 0 14px; + border: 1px solid currentColor; + border-radius: 2px; + font-size: 0.78rem; + letter-spacing: 0.06em; +} + +.ks-badge::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 999px; + background: currentColor; +} + +.ks-badge.is-detected { color: var(--ks-vermilion); } +.ks-badge.is-improved { color: var(--ks-patina); } +.ks-badge.is-ready { color: var(--ks-kinpaku); } + +.ks-tag-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.ks-tag { + display: inline-flex; + align-items: center; + min-height: 26px; + padding: 0 12px; + border: 1px solid currentColor; + border-radius: 999px; + font-size: 0.76rem; + letter-spacing: 0.04em; +} + +.ks-tag.is-detected { color: var(--ks-vermilion); } +.ks-tag.is-improved { color: var(--ks-patina); } +.ks-tag.is-neutral { color: var(--ks-text-muted); } +.ks-tag.is-ready { color: var(--ks-kinpaku); } + + +/* ============================================================ + Pagination + ============================================================ */ + +.ks-pagination { + display: flex; + align-items: center; + gap: 4px; +} + +.ks-pagination button { + min-width: 36px; + min-height: 36px; + display: grid; + place-items: center; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background: transparent; + color: var(--ks-champagne); + font-size: 0.86rem; + cursor: pointer; + transition: border-color 180ms var(--ks-ease), color 180ms var(--ks-ease); +} + +.ks-pagination button:hover { + border-color: var(--ks-kinpaku); + color: var(--ks-kinpaku); +} + +.ks-pagination button[aria-current="page"] { + border-color: var(--ks-kinpaku); + color: var(--ks-kinpaku); +} + + +/* ============================================================ + Toast / alert + ============================================================ */ + +.ks-toast { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 14px; + align-items: center; + padding: 14px 18px; + border: 1px solid currentColor; + border-radius: 2px; + max-width: 420px; +} + +.ks-toast-icon { + width: 18px; + height: 18px; + display: grid; + place-items: center; + color: currentColor; +} + +.ks-toast strong { + display: block; + color: var(--ks-champagne); + font-size: 0.92rem; + font-weight: 500; +} + +.ks-toast span:not(.ks-toast-close) { + color: var(--ks-text-muted); + font-size: 0.82rem; + display: block; + margin-top: 2px; +} + +.ks-toast.is-success { color: var(--ks-patina); } +.ks-toast.is-warning { color: var(--ks-vermilion); } + +.ks-toast-close { + width: 22px; + height: 22px; + display: grid; + place-items: center; + border: 0; + background: transparent; + color: var(--ks-text-muted); + cursor: pointer; +} + +.ks-toast-close svg { width: 12px; height: 12px; } + + +/* ============================================================ + Icon button + tooltip + ============================================================ */ + +.ks-icon-button { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border: 1px solid var(--ks-rule); + border-radius: 999px; + background: transparent; + color: var(--ks-kinpaku); + cursor: pointer; +} + +.ks-tooltip { + position: absolute; + bottom: calc(100% - 18px); + left: 50%; + transform: translateX(-50%); + width: 200px; + padding: 10px 12px; + border: 1px solid var(--ks-rule); + background: var(--ks-lacquer-deep); + color: var(--ks-champagne); + font-size: 0.8rem; + line-height: 1.4; + border-radius: 2px; + opacity: 0; + pointer-events: none; + transition: opacity 160ms var(--ks-ease); +} + +.ks-icon-button:focus-visible + .ks-tooltip { + opacity: 1; +} + + +/* ============================================================ + Modal + ============================================================ */ + +.ks-modal { + position: relative; + max-width: 440px; + padding: 28px; + border: 1px solid var(--ks-rule); + background: var(--ks-lacquer-raised); + border-radius: 2px; +} + +.ks-modal h3 { + margin: 0 0 8px; + color: var(--ks-champagne); + font-size: 1.1rem; + font-weight: 400; +} + +.ks-modal p { + margin: 0 0 22px; + color: var(--ks-text-muted); + font-size: 0.92rem; + line-height: 1.5; +} + +.ks-modal-actions { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.ks-modal-close { + position: absolute; + top: 16px; + right: 16px; + width: 24px; + height: 24px; + display: grid; + place-items: center; + border: 0; + background: transparent; + color: var(--ks-text-muted); + cursor: pointer; +} + + +/* ============================================================ + Empty state + ============================================================ */ + +.ks-empty { + display: grid; + place-items: center; + gap: 14px; + padding: 56px 28px; + text-align: center; + border: 1px dashed var(--ks-rule); + border-radius: 2px; +} + +.ks-empty-icon { + width: 36px; + height: 36px; + display: grid; + place-items: center; + color: var(--ks-kinpaku); +} + +.ks-empty strong { + color: var(--ks-champagne); + font-size: 1rem; + font-weight: 400; +} + +.ks-empty p { + margin: 0; + color: var(--ks-text-muted); + font-size: 0.86rem; +} + + +/* ============================================================ + Changelog row + ============================================================ */ + +.ks-changelog { + display: grid; + gap: 18px; +} + +.ks-changelog-entry { + display: grid; + grid-template-columns: 100px 1fr; + gap: 24px; + align-items: start; + padding-bottom: 18px; + border-bottom: 1px solid var(--ks-rule); +} + +.ks-changelog-entry:last-child { border-bottom: 0; } + +.ks-changelog-date { + color: var(--ks-text-muted); + font-family: var(--ks-mono); + font-size: 0.78rem; +} + +.ks-changelog-body strong { + color: var(--ks-champagne); + font-weight: 400; + font-size: 0.96rem; + display: block; +} + +.ks-changelog-body em { + display: inline-block; + margin-left: 8px; + padding: 1px 8px; + color: var(--ks-kinpaku); + border: 1px solid var(--ks-kinpaku); + font-style: normal; + font-size: 0.66rem; + letter-spacing: 0.18em; + text-transform: uppercase; + border-radius: 2px; +} + +.ks-changelog-body p { + margin: 4px 0 0; + color: var(--ks-text-muted); + font-size: 0.86rem; +} + + +/* ============================================================ + Site chrome (kinpaku theme) — shared header AND footer + + One definition for every kinpaku page, keyed off a single + `.kinpaku-chrome` body class instead of a per-page selector list. A + page opts into the dark-lacquer header + footer by adding + `kinpaku-chrome` to its bodyClass (alongside any page-specific class); + genuinely light pages (privacy, tutorials) simply omit it. + + This is the reusable unit: the header markup is Header.astro, the + footer markup is Footer.astro, and everything visual lives here. Don't + re-declare header/footer chrome in per-page stylesheets — the only + legitimate per-page override is a deliberate brand moment (e.g. the + home page's textured `body.home-kinpaku .site-footer`). + + Brand mark + wordmark mirror /design-system's .ks-brand / .ks-mark / + .ks-wordmark exactly, so the header is internally consistent. The + github pill and footer treatment used to be home-only / copy-pasted + per page; they now live here so every kinpaku page matches. + ============================================================ */ + +.kinpaku-chrome .site-header { + padding: 28px clamp(22px, 4vw, 56px); + background: linear-gradient(180deg, oklch(5% 0.004 95 / 0.92), oklch(5% 0.004 95 / 0.68)); + border-bottom: 1px solid oklch(78% 0.12 82 / 0.18); + backdrop-filter: blur(18px) saturate(1.25); +} + +/* Hamburger toggle — hidden on desktop, shown on mobile. The toggle expands + the nav panel below the header. Two-line glyph styled in kinpaku gold so + it reads as the brand's own control, not a generic browser affordance. */ +.site-header-menu { + display: none; + width: 36px; + height: 36px; + background: transparent; + border: 1px solid oklch(78% 0.12 82 / 0.32); + border-radius: 4px; + padding: 0; + cursor: pointer; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 6px; + flex: none; + margin-left: auto; + transition: border-color 160ms ease, background 160ms ease; +} +.site-header-menu:hover { + border-color: oklch(78% 0.12 82 / 0.55); + background: oklch(78% 0.12 82 / 0.06); +} +.site-header-menu-line { + display: block; + width: 16px; + height: 1.5px; + background: var(--ks-kinpaku); + transition: transform 200ms ease, opacity 200ms ease; +} +[data-site-header][data-nav-open="true"] .site-header-menu-line:nth-child(1) { + transform: translateY(3.75px) rotate(45deg); +} +[data-site-header][data-nav-open="true"] .site-header-menu-line:nth-child(2) { + transform: translateY(-3.75px) rotate(-45deg); +} + +/* Mobile nav drawer. The right cluster (nav + github) collapses below the + header into a vertical panel that's hidden by default and revealed when + the hamburger toggles data-nav-open="true" on the header. */ +@media (max-width: 760px) { + .kinpaku-chrome .site-header { + flex-wrap: wrap; + padding: 16px clamp(16px, 4vw, 24px); + row-gap: 12px; + } + + .site-header-menu { + display: inline-flex; + } + + /* The right cluster (nav + github) becomes a collapsible drawer. Default + hidden; revealed when the header carries data-nav-open="true". The + panel extends past the header's horizontal padding to read as a true + full-width drawer, with a solid background so it doesn't bleed into + the hero content behind it. */ + [data-site-header] .site-header-right { + order: 3; + flex-basis: 100%; + display: none; + grid-template-columns: max-content max-content 1fr; + column-gap: 10px; + flex-direction: column; + align-items: stretch; + row-gap: 0; + margin: 12px 0 -16px; + padding: 6px; + background: oklch(4% 0.004 95); + border-radius: 4px; + border-top: 1px solid oklch(78% 0.12 82 / 0.18); + } + [data-site-header][data-nav-open="true"] .site-header-right { + display: grid; + } + + .kinpaku-chrome .site-header-nav { + grid-column: 1 / -1; + flex-direction: column; + align-items: stretch; + gap: 0; + font-size: 1rem; + /* Drop sub-pages.css's horizontal-scroll mask + overflow on the nav. */ + overflow: visible; + mask-image: none; + -webkit-mask-image: none; + } + /* Each row reads as a tappable surface — full-width, generous vertical + padding, hairline separators between rows. */ + [data-site-header] .site-header-nav a { + display: block; + padding: 14px clamp(16px, 4vw, 24px); + border-bottom: 1px solid oklch(78% 0.12 82 / 0.06); + } + [data-site-header] .site-header-nav a:last-child { + border-bottom: 0; + } + + /* sub-pages.css hides Home on mobile via `a[data-nav="home"] {display:none}` + because the brand acted as the home link in the old horizontal nav. + The drawer pattern surfaces all items, so override that here. */ + [data-site-header] .site-header-nav a[data-nav="home"] { + display: block; + } + + .kinpaku-chrome .theme-toggle { + grid-column: 1; + margin: 12px 0 6px; + } + + .kinpaku-chrome .site-header-github { + grid-column: 2; + align-self: flex-start; + margin: 12px 0 6px; + } + + /* sub-pages.css hides the github star count label on mobile. The drawer + has room for it, so bring it back. */ + [data-site-header] .site-header-github-label { + display: inline; + } +} + +.kinpaku-chrome .site-header-brand { + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--ks-kinpaku); + font-family: var(--ks-font); + text-decoration: none; +} + +.kinpaku-chrome .site-header-brand-logo { + width: 38px; + height: 38px; + display: grid; + place-items: center; + color: var(--ks-kinpaku); + background: transparent; + box-shadow: none; +} + +.kinpaku-chrome .site-header-brand-logo svg { + width: 32px; + height: 32px; +} + +.kinpaku-chrome .site-header-brand-name, +.kinpaku-chrome .footer-logo { + color: var(--ks-kinpaku); + font-family: var(--ks-font-wordmark); + font-weight: 400; + font-size: 1.3rem; + letter-spacing: 0.15em; + text-transform: uppercase; + line-height: 1; + /* main.css / sub-pages.css set -webkit-font-smoothing: antialiased on + which renders thinner stems than the design-system page (which + uses the browser default). Match the design-system rendering so every + page's wordmark is visually identical, not just structurally the same. */ + -webkit-font-smoothing: auto; + -moz-osx-font-smoothing: auto; +} + +.kinpaku-chrome .site-header-nav { + gap: clamp(22px, 3vw, 48px); + font-family: var(--ks-font); + font-size: 0.95rem; + 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); +} + +.kinpaku-chrome .site-header-nav a:hover, +.kinpaku-chrome .site-header-nav a[aria-current="page"] { + color: var(--ks-kinpaku); +} + +/* GitHub star pill. Previously home-only; now every kinpaku page gets it. */ +.kinpaku-chrome .site-header-github { + border: 1px solid oklch(80% 0 0); + border-radius: 999px; + background: oklch(10% 0.006 95 / 0.74); +} + +.kinpaku-chrome .site-header-github:hover { + color: var(--ks-patina); + border-color: var(--ks-patina); + background: oklch(70% 0.105 190 / 0.08); +} + +.kinpaku-chrome .site-header-github-star { + color: var(--ks-kinpaku); +} + +/* ============================================================ + Site footer (kinpaku theme) — the default dark footer for every + kinpaku page. Was copy-pasted identically into slop/docs/designing/ + live-mode stylesheets; lives here once now. The home page layers a + textured verdigris divider on top via `body.home-kinpaku .site-footer` + (higher specificity, so it wins regardless of import order). + ============================================================ */ +.kinpaku-chrome .site-footer { + background: oklch(4% 0.004 95); + border-top: 1px solid var(--ks-rule); +} + +.kinpaku-chrome .footer-links a, +.kinpaku-chrome .footer-credit, +.kinpaku-chrome .footer-social-link { + color: var(--ks-text-muted); +} + +.kinpaku-chrome .footer-credit a { + color: var(--ks-champagne); +} + +.kinpaku-chrome .footer-links a:hover, +.kinpaku-chrome .footer-credit a:hover, +.kinpaku-chrome .footer-social-link:hover { + color: var(--ks-kinpaku); +} + +/* ============================================================ + Kinpaku surface — legacy-token remap + + For pages whose content still reads the old light tokens + (--color-paper, --color-ink, --font-display, …): instead of overriding + every rule, remap those tokens to kinpaku values at the body scope so + the existing legacy CSS renders dark for free. This is the same trick + docs-kinpaku/designing-kinpaku use per page, lifted into one reusable + class. It is also the prototype for making kinpaku the global default — + the eventual move is to lift this remap to :root and delete the light + token values entirely. + + Pair with .kinpaku-chrome (header + footer). Existing bespoke kinpaku + pages already remap in their own page stylesheet, so they don't need + this class. + ============================================================ */ +.kinpaku-surface { + --color-paper: oklch(7% 0.006 95); + --color-cream: oklch(9% 0.006 95); + --color-bg: oklch(11% 0.006 95); + --color-mist: var(--ks-rule); + --color-charcoal: var(--ks-text-muted); + --color-ash: var(--ks-text-faint); + --color-ink: var(--ks-champagne); + --color-text: var(--ks-text); + --color-accent: var(--ks-kinpaku); + --color-accent-dim: oklch(78% 0.12 82 / 0.18); + --color-accent-hover: var(--ks-kinpaku-pale); + --color-accent-soft: oklch(78% 0.12 82 / 0.32); + --font-body: var(--ks-font); + --font-display: var(--ks-font-display); + --font-mono: var(--ks-mono); + + background: linear-gradient(180deg, oklch(7% 0.006 95), oklch(4% 0.004 95)); + color: var(--ks-text); + min-height: 100vh; +} + +/* The legacy display face was italic serif; the kinpaku display face + (Alumni Sans) renders wrong as synthesized italic, so reset it on + the headings the base stylesheet set to italic. */ +.kinpaku-surface .sub-page-title, +.kinpaku-surface .prose h1, +.kinpaku-surface .prose h2 { + font-style: normal; +} + + +/* ============================================================ + Bento — multi-tile container pattern. The canonical alternative + to card-in-card. Tiles are full-width slabs on a darker rail; the + 8px gap between them shows the rail through as inset gutters. + Use with .ks-bento-tile--span-4 / --span-6 / --span-8 across a + 12-column grid. Pages can extend with full-bleed breakout or + custom edge padding (see /). + ============================================================ */ + +.ks-bento { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 8px; + background: oklch(0.17 0 0); + border-top: 8px solid oklch(0.17 0 0); + border-bottom: 8px solid oklch(0.17 0 0); +} + +.ks-bento-tile { + position: relative; + display: flex; + flex-direction: column; + gap: 14px; + padding: 48px; + background: var(--ks-lacquer); + border: 0; + border-radius: 0; + overflow: hidden; +} + +.ks-bento-tile--span-4 { grid-column: span 4; } +.ks-bento-tile--span-6 { grid-column: span 6; } +.ks-bento-tile--span-8 { grid-column: span 8; } + +@media (max-width: 980px) { + .ks-bento-tile { grid-column: 1 / -1; } +} + +/* Tile number — tiny mono caps. Defaults to kinpaku; flip individual tiles + to verdigris with data-color="patina" for tonal variety down a column. */ +.ks-bento-num { + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.28em; + text-transform: uppercase; + color: var(--ks-kinpaku); +} + +.ks-bento-num[data-color="patina"] { + color: var(--ks-patina); +} + + +/* ============================================================ + Skeleton (loading) + ============================================================ */ + +.ks-skeleton { + display: grid; + gap: 8px; + max-width: 280px; +} + +.ks-skeleton span { + height: 8px; + border-radius: 999px; + background: linear-gradient(90deg, var(--ks-graphite), oklch(22% 0.008 95), var(--ks-graphite)); + background-size: 200% 100%; + animation: ks-shimmer 1.8s linear infinite; +} + +.ks-skeleton span:nth-child(2) { width: 70%; } +.ks-skeleton span:nth-child(3) { width: 50%; } + +@keyframes ks-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + + +/* ============================================================ + Live picker (kinpaku theme) + + Shared kinpaku styling for the .live-demo-* picker mock used on the + homepage and /live-mode. The base structural CSS lives in + site/styles/live-mode.css; this block re-skins the picker chrome + (browser frame URL bar, bottom global bar, context bar, target + outline, comment tag, stroke) on dark kinpaku surfaces. + + Page-specific bits stay in the page CSS: + - .live-demo-frame background (lacquer-raised vs lacquer-deep) + - .live-demo-stage (homepage shows a gold-grid stylized stage; + /live-mode shows a light cream surface that simulates a real + dev preview) + - .live-demo-card (different content per page) + + To extend the picker to a new kinpaku-themed page, just add its body + class to the selector lists below. No duplicating CSS in page files. + + The actual live picker that injects into user dev servers + (skill/scripts/live-browser.js) is a separate runtime; its color + constants are kept in sync with these tokens by hand. + ============================================================ */ + +/* 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 oklch(92% 0 0 / 0.07); +} + +.home-kinpaku .live-demo-dot, +.live-mode-kinpaku .live-demo-dot { + width: 8px; + height: 8px; + background: oklch(34% 0 0); +} + +.home-kinpaku .live-demo-url, +.live-mode-kinpaku .live-demo-url { + margin-left: 12px; + font-family: var(--ks-mono); + font-size: 0.72rem; + color: var(--ks-text-muted); + letter-spacing: 0.04em; + background: transparent; + border: 1px solid oklch(92% 0 0 / 0.08); +} + +/* Active control surfaces — the bottom picker bar and the context bar + that floats next to the picked element. These ARE the Impeccable UI, + so they get the full-saturation kinpaku-gold border at 1.5px with a + faint matching halo, distinguishing them from chrome. */ +.home-kinpaku .live-demo-gbar, +.live-mode-kinpaku .live-demo-gbar, +.home-kinpaku .live-demo-ctx, +.live-mode-kinpaku .live-demo-ctx { + background: var(--ks-lacquer-deep); + /* 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); +} + +.home-kinpaku .live-demo-gbar-brand, +.live-mode-kinpaku .live-demo-gbar-brand { + color: var(--ks-kinpaku); + background: transparent; +} + +.home-kinpaku .live-demo-gbar-btn, +.live-mode-kinpaku .live-demo-gbar-btn { + color: var(--ks-text-muted); + background: transparent; +} + +/* 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(27% 0 0); + color: var(--ks-kinpaku); + border-radius: 6px; +} + +/* Page chat — collapsed pill with chat + mic; expands on click. */ +.home-kinpaku .live-demo-gbar-chat, +.live-mode-kinpaku .live-demo-gbar-chat { + display: inline-flex; + align-items: center; + height: 28px; + width: 104px; + margin: 0 4px 0 6px; + border-radius: 5px; + background: var(--ks-graphite); + border: 1px solid transparent; + overflow: hidden; + cursor: pointer; + flex-shrink: 0; + transition: border-color 0.15s ease; +} + +.home-kinpaku .live-demo-gbar-chat.is-expanded, +.live-mode-kinpaku .live-demo-gbar-chat.is-expanded { + width: min(220px, 34vw); + cursor: text; +} + +.home-kinpaku .live-demo-gbar-chat-icon, +.live-mode-kinpaku .live-demo-gbar-chat-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + color: var(--ks-text-muted); + pointer-events: none; +} + +.home-kinpaku .live-demo-gbar-chat-hint, +.live-mode-kinpaku .live-demo-gbar-chat-hint { + flex: 1; + min-width: 0; + font-size: 11.5px; + font-weight: 500; + color: var(--ks-text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; +} + +.home-kinpaku .live-demo-gbar-chat-input, +.live-mode-kinpaku .live-demo-gbar-chat-input { + flex: 1; + min-width: 0; + width: 0; + padding: 0; + border: 0; + background: transparent; + font: inherit; + font-size: 11.5px; + color: var(--ks-champagne); + outline: none; + opacity: 0; + pointer-events: none; + caret-color: var(--ks-kinpaku); + transition: opacity 0.15s ease; +} + +.home-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-hint, +.live-mode-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-hint { + display: none; +} + +.home-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-input, +.live-mode-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-input { + width: auto; + padding: 0 4px; + opacity: 1; + pointer-events: auto; +} + +.home-kinpaku .live-demo-gbar-chat.is-expanded .live-demo-gbar-chat-hint, +.live-mode-kinpaku .live-demo-gbar-chat.is-expanded .live-demo-gbar-chat-hint { + display: none; +} + +.home-kinpaku .live-demo-gbar-chat.is-expanded .live-demo-gbar-chat-input, +.live-mode-kinpaku .live-demo-gbar-chat.is-expanded .live-demo-gbar-chat-input { + width: auto; + padding: 0 6px; + opacity: 1; + pointer-events: auto; +} + +.home-kinpaku .live-demo-gbar-chat-input::placeholder, +.live-mode-kinpaku .live-demo-gbar-chat-input::placeholder { + color: var(--ks-text-muted); + opacity: 1; +} + +.home-kinpaku .live-demo-gbar-chat-voice, +.live-mode-kinpaku .live-demo-gbar-chat-voice { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: 0; + background: transparent; + color: var(--ks-text-muted); + cursor: pointer; + flex-shrink: 0; + transition: color 0.12s ease, background 0.12s ease; +} + +.home-kinpaku .live-demo-gbar-chat-voice:hover, +.live-mode-kinpaku .live-demo-gbar-chat-voice:hover, +.home-kinpaku .live-demo-gbar-chat-voice.is-active, +.live-mode-kinpaku .live-demo-gbar-chat-voice.is-active { + color: var(--ks-kinpaku); + background: oklch(78% 0.12 82 / 0.12); +} + +.home-kinpaku .live-demo-gbar-divider, +.live-mode-kinpaku .live-demo-gbar-divider { + background: var(--ks-rule); +} + +.home-kinpaku .live-demo-gbar-x, +.live-mode-kinpaku .live-demo-gbar-x { + color: var(--ks-text-muted); + background: transparent; +} + +.home-kinpaku .live-demo-gbar-x:hover, +.live-mode-kinpaku .live-demo-gbar-x:hover { + color: var(--ks-vermilion); +} + +/* Context bar sub-elements (the inline action panel that appears + next to a picked element during "configure" / "cycling" / "accepted" + states). */ +.home-kinpaku .live-demo-ctx[data-phase="configuring"], +.live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] { + padding: 0; + background: var(--ks-graphite-2); + overflow: hidden; +} + +.home-kinpaku .live-demo-ctx-command, +.live-mode-kinpaku .live-demo-ctx-command { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + height: 36px; + padding: 0 6px 0 7px; + background: transparent; + border: 0; + overflow: hidden; +} + +.home-kinpaku .live-demo-ctx-trailing, +.live-mode-kinpaku .live-demo-ctx-trailing { + display: inline-flex; + align-items: stretch; + flex-shrink: 0; + border-left: 1px solid oklch(92% 0 0 / 0.12); +} + +.home-kinpaku .live-demo-ctx-modifiers, +.live-mode-kinpaku .live-demo-ctx-modifiers { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 10px; + flex-shrink: 0; +} + +.home-kinpaku .live-demo-ctx-selection, +.live-mode-kinpaku .live-demo-ctx-selection { + box-sizing: border-box; + min-width: 28px; + height: auto; + padding: 1px 4px; + border: 1px solid oklch(70% 0.12 188); + border-radius: 7px; + flex-shrink: 0; + background: transparent; + color: oklch(70% 0.12 188); + font-family: var(--ks-mono); + font-size: 12px; + font-weight: 600; + line-height: 18px; + transition: background 0.15s ease, color 0.15s ease; +} + +.home-kinpaku .live-demo-ctx-action, +.home-kinpaku .live-demo-ctx-count, +.live-mode-kinpaku .live-demo-ctx-action, +.live-mode-kinpaku .live-demo-ctx-count { + box-sizing: border-box; + height: 18px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: var(--ks-text-muted); + font-size: 12px; + font-weight: 500; + line-height: 18px; + transition: color 0.12s ease; +} + +.home-kinpaku .live-demo-ctx-count, +.live-mode-kinpaku .live-demo-ctx-count { + font-family: var(--ks-mono); + font-weight: 600; +} + +.home-kinpaku .live-demo-ctx-row--configure, +.live-mode-kinpaku .live-demo-ctx-row--configure { + width: 100%; + min-height: 36px; +} + +.home-kinpaku .live-demo-ctx-actions, +.live-mode-kinpaku .live-demo-ctx-actions { + display: inline-flex; + align-items: stretch; + border-left: 1px solid oklch(92% 0 0 / 0.12); +} + +.home-kinpaku .live-demo-ctx-voice, +.live-mode-kinpaku .live-demo-ctx-voice { + width: 36px; + height: 36px; + padding: 0; + border: 0; + border-right: 1px solid oklch(92% 0 0 / 0.12); + border-radius: 0; + background: transparent; + color: var(--ks-text-muted); +} + +.home-kinpaku .live-demo-ctx-actions .live-demo-ctx-go, +.live-mode-kinpaku .live-demo-ctx-actions .live-demo-ctx-go { + width: 36px; + height: 36px; + padding: 0; + border-radius: 0; + border-left: 0; +} + +.home-kinpaku .live-demo-ctx-action-caret, +.live-mode-kinpaku .live-demo-ctx-action-caret { + font-size: 10px; + color: inherit; +} + +.home-kinpaku .live-demo-ctx-selection:hover, +.live-mode-kinpaku .live-demo-ctx-selection:hover { + background: oklch(27% 0 0); +} + +.home-kinpaku .live-demo-ctx-action:hover, +.home-kinpaku .live-demo-ctx-count:hover, +.live-mode-kinpaku .live-demo-ctx-action:hover, +.live-mode-kinpaku .live-demo-ctx-count:hover { + color: var(--ks-text); +} + +.home-kinpaku .live-demo-ctx-input, +.live-mode-kinpaku .live-demo-ctx-input { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; + height: 18px; + padding: 0; + background: transparent; + border: 0; + border-radius: 0; + font-size: 12px; + font-weight: 500; + line-height: 18px; + color: oklch(94% 0.02 82); +} + +.home-kinpaku .live-demo-ctx-caret, +.live-mode-kinpaku .live-demo-ctx-caret { + background: oklch(94% 0.02 82); +} + +.home-kinpaku .live-demo-ctx-go, +.live-mode-kinpaku .live-demo-ctx-go, +.home-kinpaku .live-demo-ctx-accept, +.live-mode-kinpaku .live-demo-ctx-accept { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); + border: 0; +} + +/* Command-palette switcher — the picker's design vocabulary. Mirrors the real + action picker in live-browser.js: a 4-column grid of icon-over-label chips, + the selected chip on a soft kinpaku wash with its icon recolored to brand. + This is the move that makes the live approach unique, so the demo shows it. */ +.home-kinpaku .live-demo-ctx-palette, +.live-mode-kinpaku .live-demo-ctx-palette { + position: absolute; + left: 6px; + bottom: calc(100% + 8px); + z-index: 2; + padding: 6px; + border-radius: 8px; + background: var(--ks-lacquer-deep); + border: 1px solid oklch(92% 0 0 / 0.13); + box-shadow: 0 18px 40px -14px oklch(0% 0 0 / 0.7); + opacity: 0; + transform: translateY(6px) scale(0.97); + transform-origin: bottom left; + pointer-events: none; + transition: opacity 160ms ease, transform 200ms cubic-bezier(0.22, 1, 0.36, 1); +} +.home-kinpaku .live-demo-ctx-palette.is-open, +.live-mode-kinpaku .live-demo-ctx-palette.is-open { + opacity: 1; + transform: none; +} +.home-kinpaku .live-demo-ctx-palette-grid, +.live-mode-kinpaku .live-demo-ctx-palette-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 3px; +} +.home-kinpaku .live-demo-ctx-palette-chip, +.live-mode-kinpaku .live-demo-ctx-palette-chip { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + width: 62px; + padding: 9px 6px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--ks-champagne); + font-family: var(--ks-font); + font-size: 11px; + font-weight: 500; + white-space: nowrap; + cursor: pointer; + transition: background 0.12s ease, color 0.12s ease; +} +.home-kinpaku .live-demo-ctx-palette-ico, +.live-mode-kinpaku .live-demo-ctx-palette-ico { + display: flex; + align-items: center; + justify-content: center; + height: 20px; + opacity: 0.82; +} +.home-kinpaku .live-demo-ctx-palette-name, +.live-mode-kinpaku .live-demo-ctx-palette-name { + line-height: 1; +} +.home-kinpaku .live-demo-ctx-palette-chip.is-highlight, +.live-mode-kinpaku .live-demo-ctx-palette-chip.is-highlight { + background: oklch(78% 0.12 82 / 0.16); + color: var(--ks-kinpaku); +} +.home-kinpaku .live-demo-ctx-palette-chip.is-highlight .live-demo-ctx-palette-ico, +.live-mode-kinpaku .live-demo-ctx-palette-chip.is-highlight .live-demo-ctx-palette-ico { + opacity: 1; +} + +.home-kinpaku .live-demo-ctx-nav, +.live-mode-kinpaku .live-demo-ctx-nav { + background: transparent; + color: var(--ks-text-muted); +} + +.home-kinpaku .live-demo-ctx-nav:hover, +.live-mode-kinpaku .live-demo-ctx-nav:hover { + color: var(--ks-kinpaku); +} + +.home-kinpaku .live-demo-ctx-counter, +.live-mode-kinpaku .live-demo-ctx-counter { + color: var(--ks-champagne); + font-family: var(--ks-mono); +} + +.home-kinpaku .live-demo-ctx-divider, +.live-mode-kinpaku .live-demo-ctx-divider { + background: var(--ks-rule); +} + +.home-kinpaku .live-demo-ctx-discard, +.live-mode-kinpaku .live-demo-ctx-discard { + background: transparent; + color: var(--ks-text-muted); +} + +.home-kinpaku .live-demo-ctx-discard:hover, +.live-mode-kinpaku .live-demo-ctx-discard:hover { + color: var(--ks-vermilion); +} + +.home-kinpaku .live-demo-ctx-spinner, +.live-mode-kinpaku .live-demo-ctx-spinner { + border-color: var(--ks-rule); + border-top-color: var(--ks-kinpaku); +} + +.home-kinpaku .live-demo-ctx-row--accepted, +.live-mode-kinpaku .live-demo-ctx-row--accepted { + color: var(--ks-kinpaku); +} + +/* Target outline + annotations (stroke + comment tag) — kinpaku. */ +.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); + /* 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, +.live-mode-kinpaku .live-demo-annotations { + color: var(--ks-kinpaku); + border-color: var(--ks-kinpaku); +} + +.home-kinpaku .live-demo-comment, +.live-mode-kinpaku .live-demo-comment { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); +} + +.home-kinpaku .live-demo-stroke, +.live-mode-kinpaku .live-demo-stroke { + color: var(--ks-kinpaku); +} diff --git a/picker/styles/vendor/kinpaku-tokens.css b/picker/styles/vendor/kinpaku-tokens.css new file mode 100644 index 000000000..18993ef40 --- /dev/null +++ b/picker/styles/vendor/kinpaku-tokens.css @@ -0,0 +1,205 @@ +/* + * impeccable.style — Neo kinpaku tokens (page-global, single source of truth) + * + * Every brand-bearing color, surface, text color, hairline rule, type-scale + * value, and motion easing used on the site lives here. New pages should read + * these tokens via var(--ks-*) rather than hand-typing oklch values. + * + * Pages may override individual tokens locally when there's a documented + * reason (e.g. .home-kinpaku bumps --ks-rule alpha for sharper UI borders + * against bento tiles). When in doubt, do not override — match. + * + * Imported by design-system.css, home-kinpaku.css, and designing-kinpaku.css. + * If a new page-level stylesheet needs these tokens, @import this file at the + * top so the values are available at :root. + */ + +:root { + /* color-scheme follows html.light / html.dark class on */ + /* ============================================================ + Brand anchors. Two anchors carry the brand — kinpaku gold for + commitment and brand voice, verdigris patina for state and + contrast. Everything else is supporting role. + ============================================================ */ + + /* Kinpaku gold. The rich, warm, vibrant primary. */ + --ks-kinpaku: oklch(84% 0.19 80.46); /* primary accent, wordmark, icon */ + --ks-kinpaku-pale: oklch(86% 0.07 84); /* hover lift, pale fills */ + --ks-kinpaku-rich: oklch(77% 0.13 82); /* active CTA, severity-medium */ + --ks-kinpaku-deep: oklch(61% 0.085 78); /* borders against the brand */ + + /* Verdigris patina. The secondary accent — state, signal, selection. */ + --ks-patina: oklch(70% 0.12 188); /* secondary accent */ + --ks-patina-pale: oklch(82% 0.07 188); /* hover lift on patina */ + --ks-patina-deep: oklch(49% 0.08 188); /* deep oxide, dark variants */ + + /* Warning — vermilion. Used sparingly, only for failures and warnings. */ + --ks-vermilion: oklch(58% 0.15 35); + + /* ============================================================ + Surfaces. Warm-black lacquer carries everything. + ============================================================ */ + --ks-lacquer: oklch(7% 0.006 95); /* page ground */ + --ks-lacquer-deep: oklch(4% 0.004 95); /* deepest inset */ + --ks-lacquer-raised: oklch(11% 0.006 95); /* raised panels, inputs */ + --ks-graphite: oklch(15% 0.008 95); /* inactive surfaces */ + --ks-graphite-2: oklch(19% 0.008 95); /* one step up from graphite */ + + /* ============================================================ + 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(91% 0 0); /* headlines, — 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. The hairline is neutral so borders/dividers/labels don't + carry warmth (warmth lives in the gold accents and surfaces). For a + gold accent border, use --ks-kinpaku directly, not a readability variant. + ============================================================ */ + --ks-rule: oklch(78% 0 0 / 0.16); + + /* ============================================================ + 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 — two faces. + - --ks-font-display: Alumni Sans. A weightable humanist grotesque; + the hero h1 runs at a thin hairline (100) for elegance, section + h2s at a heavier cut (300) to anchor each block. + - --ks-font-wordmark: Alumni Sans, same face as display, held at + weight 400 for the brand lockup. + - --ks-font: Albert Sans. Quiet geometric humanist, used for body + and UI text. Pairs cleanly with the Alumni Sans display without + competing. + All loaded via Base.astro's Google Fonts link. + ============================================================ */ + --ks-font-display: "Alumni Sans", "Albert Sans", Arial, sans-serif; + --ks-font-wordmark: "Alumni Sans", "Albert Sans", Arial, sans-serif; + --ks-font: "Albert Sans", "Avenir Next", "Helvetica Neue", Arial, system-ui, sans-serif; + --ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace; + + /* ============================================================ + Type scale. These are the actual sizes the homepage ships with — + new pages should read them via var(...) rather than hand-typing. + The display vs headline weight split is deliberate: the hero h1 + reads thin (100) for elegance; section h2s read heavier (300) to + anchor each block. Section h2s look heavier than the h1 on purpose. + ============================================================ */ + + /* Display — hero h1. */ + --ks-type-display-size: clamp(3.4rem, 6.5vw, 5.6rem); + --ks-type-display-weight: 100; + --ks-type-display-line: 1.02; + --ks-type-display-track: -0.01em; + + /* Headline — section h2. Heavier than display. */ + --ks-type-headline-size: clamp(2.6rem, 4vw, 3.4rem); + --ks-type-headline-weight: 300; + --ks-type-headline-line: 1.04; + + /* Title — card and panel headings (h3). */ + --ks-type-title-size: 1.18rem; + --ks-type-title-weight: 500; + --ks-type-title-line: 1.35; + + /* Body. */ + --ks-type-body-size: 1.02rem; + --ks-type-body-line: 1.8; + + /* Eyebrow — small mono labels above titles. */ + --ks-type-eyebrow-size: 0.7rem; + --ks-type-eyebrow-track: 0.18em; + + /* Wordmark — IMPECCABLE in the header. */ + --ks-type-wordmark-size: 1.15rem; + --ks-type-wordmark-track: 0.42em; + + /* Mono — code, terminal, audit lines. */ + --ks-type-mono-size: 0.72rem; + --ks-type-mono-track: 0.22em; + + /* ============================================================ + Motion. + ============================================================ */ + --ks-ease: cubic-bezier(0.2, 0.8, 0.2, 1); +} + +html.dark { + color-scheme: dark; +} + +/* ============================================================ + Light mode — pale surfaces, dark ink type, and kinpaku kept as the + real brand gold. Activated via html.light on + (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). + For decorative small text on paper, use patina. Use --ks-link-on-paper + for body-sized links that need stronger contrast. */ + --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: var(--ks-patina); + + /* 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); + + /* 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-link-on-paper); +} diff --git a/scripts/build-picker.mjs b/scripts/build-picker.mjs index 1b5265f77..c1357e796 100644 --- a/scripts/build-picker.mjs +++ b/scripts/build-picker.mjs @@ -3,18 +3,16 @@ import { execFileSync } from 'node:child_process'; import { cp, copyFile, mkdir, rm } from 'node:fs/promises'; import path from 'node:path'; -import sharp from 'sharp'; import { fileURLToPath } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const buildDir = path.join(root, 'build-picker'); const outputDir = path.join(root, 'skill/scripts/picker'); -const faviconSource = path.join(root, 'site/public/favicon.svg'); +// Static assets vendored under picker/assets/ (the site now lives in the +// private impeccable-site repo, so the picker carries its own copies). +const faviconSource = path.join(root, 'picker/assets/favicon.svg'); const faviconOutput = path.join(outputDir, 'favicon.svg'); -const heroSource = path.join( - root, - 'site/public/assets/neo-kinpaku/candidates/finalists/m-01-v2-01-light.png', -); +const heroSource = path.join(root, 'picker/assets/hero-light.jpg'); const heroOutput = path.join(outputDir, 'assets/hero-light.jpg'); await rm(buildDir, { recursive: true, force: true }); @@ -28,10 +26,7 @@ await rm(outputDir, { recursive: true, force: true }); await cp(buildDir, outputDir, { recursive: true }); await mkdir(path.dirname(heroOutput), { recursive: true }); await copyFile(faviconSource, faviconOutput); -await sharp(heroSource) - .resize({ width: 1536, withoutEnlargement: true }) - .jpeg({ quality: 80, mozjpeg: true }) - .toFile(heroOutput); +await copyFile(heroSource, heroOutput); await rm(buildDir, { recursive: true, force: true }); console.log(`Built ${path.relative(root, outputDir)}/`); diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index d7577962b..1beabb4c9 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -701,7 +701,7 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki * their command prefix from lib/provider.mjs, whose declaration is replaced * here by an exact string match. */ -export function replaceScriptProviderMarker(content, provider) { +export function replaceScriptProviderMarker(content, provider, buildProvider = provider) { if (Buffer.isBuffer(content)) return content; const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS.cursor; const commandPrefix = placeholders.command_prefix || '/'; diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index 03798b52d..897c4a68c 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -28,7 +28,6 @@ export const SUITES = { /^picker\//, /^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/, /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|picker|pin|surface-brief))/, - /^site\/(pages|content|components|layouts)\//, /^README(\.npm)?\.md$/, /^cli\/bin\//, ], diff --git a/skill/reference/craft.md b/skill/reference/craft.md index 768d49772..dbbc9402c 100644 --- a/skill/reference/craft.md +++ b/skill/reference/craft.md @@ -2,122 +2,4 @@ `craft` is a deprecated alias for an ordinary request to make new visual work. It adds no setup, interview, checkpoint, tool, or quality behavior. Apply SKILL.md's normal routing: create missing PRODUCT.md through [init.md](init.md), then follow [new-work.md](new-work.md) for visual authority, world and surface decisions, implementation, and finish. -Before writing code, you need: PRODUCT.md loaded, register identified and the matching reference loaded, and a confirmed design direction for this task (either from `shape` or supplied by the user). PRODUCT.md is project context, not a task-specific brief. - -Treat any approved visual direction (generated mock or stated reference) as a concrete contract for composition, hierarchy, density, atmosphere, signature motifs, and distinctive visual moves. Don't let mocks replace structure, copy, accessibility, or state design. But if the live result lacks the approved direction's major ingredients, the implementation is wrong. - -### Gates: do not compress - -Craft has **multiple user gates**, not one. When the harness has native image generation (Codex via `image_gen`), the gate sequence before code is: - -1. **Shape brief confirmed** (Step 1) -2. **Direction questions answered** (codex.md Step A) -3. **Palette confirmed** (codex.md Step B) -4. **One mock direction approved or delegated** (codex.md Step D) - -You must stop at every gate. **Shape confirmation alone is NOT a green light to start coding.** It is the green light to begin codex.md Step A. Compressing gates 2 through 4 because the shape brief felt complete is the dominant failure mode of this flow. - -When the harness lacks native image generation, gates 2-4 collapse into the brief itself, and shape confirmation does advance straight to code. - -## Step 0: Project Foundation - -Before shape, before code: figure out what kind of project you're working in. - -Look at the working directory. Run `ls`. Check for: - -- An existing framework: `astro.config.mjs/ts`, `next.config.js/ts`, `nuxt.config.ts`, `svelte.config.js`, `vite.config.js/ts`, `package.json` with framework deps, `Cargo.toml` + Leptos/Yew, `Gemfile` + Rails. **If found, use it.** Do not start a parallel build, do not introduce a second framework, do not write to `dist/` or `build/` directly. Whatever pipeline the project has, respect it. -- An existing component library or design system: `src/components/`, `app/components/`, a `tokens.css` / `theme.ts`, an `astro.config` `integrations`. Read what's there before adding to it. -- An existing icon set: `lucide-react`, `@phosphor-icons/react`, `@iconify/*`, hand-rolled SVG sprites in `assets/icons/`. **Use what's already in the project**; don't introduce a second set. - -If the directory is empty (greenfield), don't pick a framework silently. Ask the user via the AskUserQuestion tool, with sensible defaults framed by the brief: - -```text -What should this be built on? - - Astro (default for content-led brand sites, landing pages, marketing surfaces) - - SvelteKit / Next.js / Nuxt (when the brief implies an app surface or significant interactivity) - - Single index.html (one-shot demo, prototype, or a deliberately framework-free experiment) -``` - -Default: Astro for brand briefs, the project's existing framework for product briefs. Ask once; don't re-ask mid-task. - -## Step 1: Shape the Design - -Run {{command_prefix}}impeccable shape, passing along whatever feature description the user provided. Shape is **required** for craft; it is what produces a confirmed direction. - -Present the shape output and stop. Wait for the user to confirm, override, or course-correct before writing code. - -If the user already supplied a confirmed brief or ran shape separately, use it and skip this step. - -When the original prompt + PRODUCT.md already answer scope, content, and visual direction with no real ambiguity, the shape output can be **compact** (3-5 bullets stating what you're building and the visual lane, ending with one or two specific questions or "confirm or override"). The full 10-section structured brief is reserved for genuinely ambiguous, multi-screen, or stakeholder-heavy tasks. Don't pad a clear brief into a long one to look thorough; equally, don't skip the pause to look efficient. - -If the harness has native image generation (Codex), a compact shape's "confirm or override" advances to **Step 3 and the codex.md flow**, not to Step 4. Phrase the closing line accordingly: "Confirm or override; once we lock direction, I'll run a couple of palette and reference questions before generating any mocks." This stops the model from reading shape confirmation as code-green. - -## Step 2: Load References - -Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: - -- [layout.md](layout.md) for layout, spacing, grid, container queries, optical adjustments -- [typeset.md](typeset.md) for type hierarchy, font selection, web font loading, OpenType features (Reference Material section) - -Then add references based on the brief's needs: -- Complex interactions or forms? Consult [interaction-design.md](interaction-design.md) -- Animation or transitions? Consult [animate.md](animate.md) (Reference Material covers motion materials, durations, easing, perceived performance) -- Color-heavy or themed? Consult [colorize.md](colorize.md) (Reference Material covers OKLCH, palette structure, dark mode, contrast) -- Responsive requirements? Consult [adapt.md](adapt.md) (Reference Material covers breakpoints, input methods, safe areas, responsive images) -- Heavy on copy, labels, or errors? Consult [clarify.md](clarify.md) (Reference Material covers button labels, error formula, voice/tone, translation) - -## Step 3: Visual Direction & Assets (Harness-Gated) - -If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4. - -If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract. - -Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery. - -## Step 4: Build to Production Quality - -**Precondition.** If Step 3 routed you to codex.md (native image generation available), Steps A through D in that file must be complete before any code: questions answered, palette confirmed, mocks generated, one direction approved or delegated. **Do not mention implementation, file paths, or patch plans until that's done.** A confirmed shape brief is not enough; the model that compressed those gates is the model that already failed this flow. - -Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration. - -### Production bar - -- **Real content.** No placeholder copy, placeholder images, dead links, fake controls, or unused scaffold at presentation time. -- **Preserve the approved mock's major ingredients.** Missing hero objects, world/product imagery, section structure, CTA/nav treatment, or distinctive motifs are blocking defects unless the user accepted the change. -- **Semantic first.** Real headings, landmarks, labels, form associations, button/link semantics, accessible names, state announcements where needed. -- **Deliberate spacing and alignment.** No default gaps, arbitrary margins, unbalanced whitespace, or accidental optical misalignment. -- **Intentional typography.** Chosen loading strategy, clear hierarchy, readable measure, stable line breaks, no overflow at any width. -- **Realistic state coverage.** Default, hover, focus-visible, active, disabled, loading, error, success, empty, overflow, long/short text, first-run. -- **Finished interaction quality.** Keyboard paths, touch targets, feedback timing, scroll behavior, state transitions, no hover-only functionality. -- **Coherent icon set.** Use the project's established set; otherwise pick one library or use accessible text. Don't mix. -- **Respect the build pipeline.** Edit source files and run the project's build (`npm run build` or equivalent). Don't write to `build/` / `dist/` / `.next/` with `cat`, heredoc, or Bash redirects; that skips asset hashing, image optimization, code splitting, and CSS extraction, and produces output the dev server won't serve. -- **Verify image URLs before referencing them.** Use image-search MCP or web-fetch when available; guessed photo IDs ship as broken-image placeholders. Without verification, prefer fewer images you're confident about. -- **Optimized imagery and media.** Correct dimensions, useful alt text, lazy loading below the fold, modern formats when practical, responsive `srcset`/`picture` for raster, no project-referenced asset left outside the workspace. -- **Premium motion.** Use atmospheric blur, filter, mask, shadow, reveal when they improve the experience. Avoid casual layout-property animation, bound expensive effects, verify smoothness in-browser, respect reduced motion, and avoid choreography that blocks task completion. -- **Maintainable.** Reusable local patterns, clear component boundaries, project conventions. No rasterized UI text or one-off hacks when a local pattern exists. -- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths. -- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess. - -## Step 5: Iterate Visually - -Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation. - -If your tool returns a file path, read the PNG back into the conversation. A screenshot you didn't read doesn't count. - -For long-form brand surfaces, inspect major sections individually. Thumbnails hide spacing, clipping, and cascade defects. - -After the first pass, write an honest critique against the brief, the approved mock's major ingredients (hero silhouette, motifs, imagery, nav/CTA, density), and impeccable's DON'Ts. Patch material defects and re-inspect. **Don't invent defects to demonstrate iteration.** A confident "first pass clean, shipping" beats a fake fix. - -Actively check: responsive behavior (composes, not shrinks), every state (empty / error / loading / edge), craft details (spacing, alignment, hierarchy, contrast, motion timing, focus), performance basics. The exit bar: defensible in a high-end studio review. - -Detector or QA output is defect evidence only; never proof the work is finished. - -## Step 6: Present - -Present the result to the user: -- Show the feature in its primary state -- Summarize the browser/viewports checked and the most important fixes made after inspection -- Walk through the key states (empty, error, responsive) -- Explain design decisions that connect back to the design brief and, when used, the chosen north-star mock. Include any accepted deviations from the mock; do not hide unimplemented mock ingredients. -- Note any remaining limitations or follow-up risks honestly -- Ask: "What's working? What isn't?" +Do not tell users they need to invoke `craft`. Natural requests such as “build this feature,” “make a landing page,” or “redesign this screen” use the same flow. diff --git a/skill/reference/document.md b/skill/reference/document.md index 06f6dfa5c..f625fb6ac 100644 --- a/skill/reference/document.md +++ b/skill/reference/document.md @@ -75,7 +75,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user - **Scan mode** (default): the project has design tokens, components, or rendered output. Extract, then confirm descriptive language. Use when there's code to analyze. - **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Gather any existing brand assets, interview for five high-level answers, optionally generate visual cues for the palette pick, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. -Decide by scanning first (Scan mode Step 1). If the scan finds no tokens, no component files, and no rendered site, offer seed mode; don't silently switch. `/impeccable document --seed` requests new-work's world workshop, but it does not authorize replacing coherent code: when an incumbent system exists, offer scan mode or route an explicit identity-replacement request through new-work. +Decide by scanning first (Scan mode Step 1). If the scan finds no tokens, no component files, and no rendered site, offer seed mode; don't silently switch. `/impeccable document --seed` forces seed mode on a pre-implementation project, but it does not authorize replacing coherent code: when an incumbent system exists, offer scan mode or route an explicit identity-replacement request through new-work. ## Scan mode (approach C: auto-extract, then confirm descriptive language) @@ -349,7 +349,9 @@ Your own write is the freshest source; subsequent commands in this session don't ## Seed mode -For projects with no visual system to extract yet. Produces a user-chosen visual-world scaffold, not a fabricated token spec. +For projects with no visual system to extract yet. Produces a minimal, user-chosen scaffold, not a fabricated token spec. + +PRODUCT.md is the prerequisite. If it is missing, load [init.md](init.md) and complete its product interview first. Do not create a visual identity without durable product context. ### Step 1: Confirm seed mode and ask for assets @@ -412,7 +414,7 @@ When generation is available, **stop and load [visual-cues.md](visual-cues.md)** ### Step 5: Write seed DESIGN.md -Use the six-section spec from Scan mode. Populate what the interview, the assets, and any cue pick answer; leave the rest as honest placeholders. The seed is a scaffold, not a fabricated spec. +Use the canonical section order from Scan mode. Populate what the interview, the assets, and any cue pick answer; leave the rest as honest placeholders. The seed is a scaffold, not a fabricated spec. Lead the file with: @@ -425,9 +427,10 @@ Per-section guidance in seed mode: - **Overview**: Creative North Star and philosophy phrased from the answers (color strategy + motion energy + references). Reference the user's anti-reference directly. - **Colors**: Color strategy as a Named Rule (e.g. *"The Drenched Rule. The surface IS the color."*). Hue family or anchor reference. Colors sampled from a provided logo, or from a cue image the user picked in Step 4, are real; include them with exact values and note the source. Everything else stays `[to be resolved during implementation]`; those sampled anchors are the only hex a seed may carry. - **Typography**: the direction the user picked (e.g. "Serif display + sans body"). No font names yet: `[font pairing to be chosen at implementation]`. -- **Elevation**: inferred from motion energy. Restrained/Responsive → flat by default; Choreographed → layered. One sentence. +- **Layout** and **Shapes**: omit unless an asset or answer established a spatial or form preference; do not invent grids or corner language pre-implementation. +- **Elevation & Depth**: inferred from motion energy. Restrained/Responsive → flat by default; Choreographed → layered. One sentence. - **Components**: omit entirely; no components exist yet. -- **Do's and Don'ts**: record the durable guardrails confirmed during the world choice, not task-local refusals. +- **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. Seed mode writes a minimal frontmatter with `name` and `description` only; no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `.impeccable/design.json` sidecar in seed mode for the same reason: nothing to render. diff --git a/skill/reference/shape.md b/skill/reference/shape.md index 535c5b57a..90a74ae81 100644 --- a/skill/reference/shape.md +++ b/skill/reference/shape.md @@ -56,107 +56,4 @@ Use three to five bullets when the task is settled; use the full structure only Present the brief for explicit confirmation or one correction round, then stop: shape never writes code or a direction contract. -### Constraints -- Are there technical constraints? (Framework, performance budget, browser support) -- Are there content constraints? (Localization, dynamic text length, user-generated content) -- Mobile/responsive requirements? -- Accessibility requirements beyond WCAG AA? - -### Anti-Goals -- What should this NOT be? What would be a wrong direction? -- What's the biggest risk of getting this wrong? - -## Phase 1.5: Visual Direction Probe (Capability-Gated) - -After the discovery interview, generate a small set of visual direction probes **before** writing the final brief when all of these are true: - -- The work is **net-new** or directionally ambiguous enough that visual exploration will clarify the brief. -- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning. -- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling. - -When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate. - -Use probes to explore visual lanes, not to replace the brief. - -Do not skip probes because the final UI will be semantic, editable, code-native, responsive, or accessible. Those are implementation requirements, not reasons to avoid visual exploration. - -### What to generate - -Generate **2 to 4** distinct direction probes based on the discovery answers, especially: - -- Color strategy -- Theme scene sentence -- Named anchor references -- Scope and fidelity - -The probes should differ in primary visual direction (hierarchy, topology, density, typographic voice, or color strategy), not just palette tweaks. - -### How to use the probes - -- Treat them as **direction tests**, not final designs. -- Use them to pressure-test whether the brief is pointing at the right lane. -- Ask the user which direction feels closest, what feels off, and what should carry forward. -- If the probes reveal a mismatch, revise the brief inputs before finalizing the brief. - -### Important limits - -- Do **not** skip discovery because image generation is available. -- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior. -- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice. - -If image generation isn't natively available, announce the skip in one line and proceed to the design brief. - -## Phase 2: Design Brief - -After the interview and any required probes, present a brief and **end your response**. The user must confirm before any implementation runs. Do not present a brief and then continue to code in the same response, even if the brief feels obvious to you. The user's confirmation is the gate. - -**Choose the brief shape based on how clear the answers are:** - -- **Compact form (3-5 bullets)** when discovery was crisp and the original prompt + PRODUCT.md already pinned scope, content, and direction. State what you're building, the visual lane, and end with one or two specific questions or a clear "confirm or override?" prompt. This is the default for typical craft requests with a clear prompt. -- **Full structured form (sections below)** when the task is genuinely ambiguous, multi-screen, or when the user asked for shape as a standalone step. Use this when the discipline of structure earns its weight. - -Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point. - -Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation. - -### Brief Structure - -**1. Feature Summary** (2-3 sentences) -What this is, who it's for, what it needs to accomplish. - -**2. Primary User Action** -The single most important thing a user should do or understand here. - -**3. Design Direction** -Color strategy (Restrained / Committed / Full palette / Drenched) + the theme scene sentence + 2–3 named anchor references. Reference PRODUCT.md and DESIGN.md where they already answer, and note any per-surface overrides. - -If you ran the Visual Direction Probe step, name which probe direction won and what changed in the brief because of it. - -**4. Scope** -Fidelity, breadth, interactivity, and time intent from the Scope section of the interview. Task-scoped; these don't persist beyond the brief. - -**5. Layout Strategy** -High-level spatial approach: what gets emphasis, what's secondary, how information flows. Describe the visual hierarchy and rhythm, not specific CSS. - -**6. Key States** -List every state the feature needs: default, empty, loading, error, success, edge cases. For each, note what the user needs to see and feel. - -**7. Interaction Model** -How users interact with this feature. What happens on click, hover, scroll? What feedback do they get? What's the flow from entry to completion? - -**8. Content Requirements** -What copy, labels, empty state messages, error messages, and microcopy are needed. Note any dynamic content and its realistic ranges. For image-led surfaces, also list the required image/media roles and their likely source (project asset, generated raster, semantic SVG/CSS, canvas/WebGL, icon library, or accepted omission). - -**9. Recommended References** -Based on the brief, list which impeccable reference files would be most valuable during implementation (e.g., layout.md for complex layouts, animate.md for animated features, interaction-design.md for form-heavy features, typeset.md for typography-driven pages, colorize.md for color-led brands). - -**10. Open Questions** -Anything genuinely unresolved. Don't list "open questions" you've already recommended a default for; assert the default and move on. If you'd write `Recommend: X` next to a question, just decide X. - ---- - -{{ask_instruction}} Ask for explicit confirmation of the brief before finishing. - -If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction. - -Once confirmed, the brief is complete. The user can now hand it to {{command_prefix}}impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use {{command_prefix}}impeccable craft instead, which runs this command internally.) +When no human or structured answer mechanism exists, mark assumptions plainly, return the brief, and stop. diff --git a/skill/scripts/palette.mjs b/skill/scripts/palette.mjs index 0d524cba2..d9b0420cc 100644 --- a/skill/scripts/palette.mjs +++ b/skill/scripts/palette.mjs @@ -420,8 +420,6 @@ const SEEDS = [ strategy: "Pure white surface so the rose-pink primary carries all the brand warmth, paired with a near-black ink and a desaturated mauve accent for editorial restraint." }, ]; -export { SEEDS }; - function parseArgs(argv) { const args = { id: null, from: null }; for (let i = 0; i < argv.length; i++) { @@ -497,139 +495,142 @@ function hueWord(H) { // --------------------------------------------------------------- +// The picker server imports SEEDS to serve /palettes.json; the CLI tail +// below only runs when this file is the entry point, so importing it has +// no side effects. +export { SEEDS }; + if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - const args = parseArgs(process.argv.slice(2)); - const seed = pickSeed(SEEDS, args); - const [L, C, H] = seed.oklch; +const args = parseArgs(process.argv.slice(2)); +const seed = pickSeed(SEEDS, args); +const [L, C, H] = seed.oklch; - // The mood + strategy on each seed were derived by the model that - // originally judged it. We surface them as *hints*, not commands — - // the brief should still drive what the seed becomes. - const moodHint = seed.mood ? ` (one read: "${seed.mood}")` : ''; - const strategyHint = seed.strategy ? `\n - one example strategy: ${seed.strategy}` : ''; +// The mood + strategy on each seed were derived by the model that +// originally judged it. We surface them as *hints*, not commands — +// the brief should still drive what the seed becomes. +const moodHint = seed.mood ? ` (one read: "${seed.mood}")` : ''; +const strategyHint = seed.strategy ? `\n - one example strategy: ${seed.strategy}` : ''; - // --------------------------------------------------------------- - // Fat tool-exit response — what the model sees on stdout. - // --------------------------------------------------------------- +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- - process.stdout.write(`BRAND SEED · ${seed.id} +process.stdout.write(`BRAND SEED · ${seed.id} - Seed color (anchor for your primary brand color): - ${fmtOklch(seed.oklch)} — ${hueWord(H)}${moodHint} +Seed color (anchor for your primary brand color): + ${fmtOklch(seed.oklch)} — ${hueWord(H)}${moodHint} - This is the brand's anchor — a single beautiful color. Compose the rest of - the palette around it using YOUR judgment, the brief (PRODUCT.md / - DESIGN.md / the user's prompt), and the color-strategy guidance already in - SKILL.md. +This is the brand's anchor — a single beautiful color. Compose the rest of +the palette around it using YOUR judgment, the brief (PRODUCT.md / +DESIGN.md / the user's prompt), and the color-strategy guidance already in +SKILL.md. - How to use: +How to use: - 1. Read the brief. Write one specific phrase describing the mood this - product calls for. Be granular. Good: "1970s travel poster — sun-baked - warmth, considered", "midnight jazz club — smoky brass, saxophone - light", "Scandinavian winter morning — quiet light through frost". Bad: - "modern and clean", "warm and inviting". The first lets you compose; the - second is generic and will produce generic palettes. +1. Read the brief. Write one specific phrase describing the mood this + product calls for. Be granular. Good: "1970s travel poster — sun-baked + warmth, considered", "midnight jazz club — smoky brass, saxophone + light", "Scandinavian winter morning — quiet light through frost". Bad: + "modern and clean", "warm and inviting". The first lets you compose; the + second is generic and will produce generic palettes. - 2. The seed's hue (${H.toFixed(0)}°) anchors your primary brand color. You - choose L and C to match the mood. The same hue can be deep-and-velvet, - bright-and-confident, or pale-and-faded — pick the one the mood demands. - Primary's hue should stay within ±10° of the seed.${strategyHint} +2. The seed's hue (${H.toFixed(0)}°) anchors your primary brand color. You + choose L and C to match the mood. The same hue can be deep-and-velvet, + bright-and-confident, or pale-and-faded — pick the one the mood demands. + Primary's hue should stay within ±10° of the seed.${strategyHint} - 3. Now compose the full palette in OKLCH (5 more roles): - • bg — the most important architectural choice. - CORE PRINCIPLE: the mood lives in the BRAND COLORS - (primary + accent) and typography, NOT in the surface. - Stripe is warm — its purple does that, bg is pure - white. Linear is cool — its blue does that, bg is - pure. Notion is warm — its accents do that, bg is - near-pure-white. Putting warmth in BOTH primary AND - bg is the AI cliché. +3. Now compose the full palette in OKLCH (5 more roles): + • bg — the most important architectural choice. + CORE PRINCIPLE: the mood lives in the BRAND COLORS + (primary + accent) and typography, NOT in the surface. + A warm brand puts the warmth in its primary against a + pure surface. Putting warmth in BOTH primary AND bg is + the AI cliché. - DEFAULT A — PURE white: exactly oklch(1.000 0.000 0). - Not 0.99, not chroma 0.002. Stripe / Notion / Apple - use literal #ffffff. Don't add hidden warmth. - Refs: Stripe, Notion, Linear (light), Apple.com, - Vercel docs, Figma marketing, Loom, Substack. + DEFAULT A — PURE white: exactly oklch(1.000 0.000 0). + Not 0.99, not chroma 0.002. The most confident + brands in every field — fashion houses, galleries, + publishers, tool makers — use literal #ffffff. + Don't add hidden warmth. - DEFAULT B — PURE black/near-black: L 0.04-0.12, - chroma exactly 0.000. No hue tint. Vercel is - roughly oklch(0.08 0 0). Pick L for mood; C is 0. - Refs: Vercel, A24, Acne, Apple dark, MUBI. + DEFAULT B — PURE black/near-black: L 0.04-0.12, + chroma exactly 0.000. No hue tint. Pick L for the + mood (cinema dark, gallery dark, instrument-panel + dark); C stays 0. - ALT 2 — TINTED: chroma 0.015-0.05. - Use ONLY when: - (a) the mood is EXPLICITLY environmental — the surface - IS part of the brand (1920s lacquered interior, - leather library, ceramic studio, hotel lobby), or - (b) the seed itself is desaturated (chroma < 0.10) and - needs a tinted surface to read as a brand. - NOT for "feels warm" / "modern + warm" / "moody". If - your mood says "warm" but doesn't name a specific - environment, use PURE white and let primary carry - the warmth. + ALT 2 — TINTED: chroma 0.015-0.05. + Use ONLY when: + (a) the mood is EXPLICITLY environmental — the surface + IS part of the brand (1920s lacquered interior, + leather library, ceramic studio, hotel lobby), or + (b) the seed itself is desaturated (chroma < 0.10) and + needs a tinted surface to read as a brand. + NOT for "feels warm" / "modern + warm" / "moody". If + your mood says "warm" but doesn't name a specific + environment, use PURE white and let primary carry + the warmth. - HEURISTIC: if seed chroma > 0.10 AND mood is product- - focused (not environment-focused), it's almost always - PURE white. Target distribution across many palettes: - ~50% pure white, ~25% pure black, ~25% tinted. - • surface — bg pulled slightly toward ink (10-15% mix). Same hue - family as bg. Used for cards, panels, sections. - • ink — body text color. Must reach ≥7:1 contrast vs bg. - Can carry the brand hue at low chroma in light mode - (slight warmth or coolness toward the brand). - • accent — a SECOND brand color, distinct from primary in BOTH - hue AND lightness. Picked to complement the mood (not - default-complementary across the wheel). Used for - badges, status pills, links, accent rules. - • muted — secondary text. Ink pulled 40% toward bg, keeping ink's - hue. Must reach ≥3.5:1 contrast vs bg. + HEURISTIC: if the seed's chroma > 0.10 and the mood + doesn't name a specific environment, it's almost + always PURE white. Target distribution across many + palettes: ~50% pure white, ~25% pure black, ~25% + tinted. + • surface — bg pulled slightly toward ink (10-15% mix). Same hue + family as bg. Used for cards, panels, sections. + • ink — body text color. Must reach ≥7:1 contrast vs bg. + Can carry the brand hue at low chroma in light mode + (slight warmth or coolness toward the brand). + • accent — a SECOND brand color, distinct from primary in BOTH + hue AND lightness. Picked to complement the mood (not + default-complementary across the wheel). Used for + badges, status pills, links, accent rules. + • muted — secondary text. Ink pulled 40% toward bg, keeping ink's + hue. Must reach ≥3.5:1 contrast vs bg. - 4. Pick a color STRATEGY (the four steps from SKILL.md): - • Restrained: tinted neutrals + accent ≤10% — product default - • Committed: one saturated color carries 30-60% — identity-driven - • Full palette: 3-4 named roles each used deliberately — brand work - • Drenched: the surface IS the color — campaign, hero, statement - The brief picks the strategy. A startup dashboard ≠ a perfume brand. +4. Pick a color STRATEGY (the four steps from SKILL.md): + • Restrained: tinted neutrals + accent ≤10% — product default + • Committed: one saturated color carries 30-60% — identity-driven + • Full palette: 3-4 named roles each used deliberately — brand work + • Drenched: the surface IS the color — campaign, hero, statement + The brief picks the strategy. A startup dashboard ≠ a perfume brand. - Hard rules (already in SKILL.md, recapped because the seed step is where - they actually bite): +Hard rules (already in SKILL.md, recapped because the seed step is where +they actually bite): - - OKLCH only — never hex. Never #RRGGBB. - - ink-vs-bg WCAG contrast ≥ 7 (body text must be readable) - - primary chroma ≤ 0.23 (above this, primary glows perceptually and - no text on it is readable — acid-bright is a UI failure) - - if primary L > 0.78, primary chroma ≤ 0.18 (the fluorescent zone) - - primary-vs-accent contrast ≥ 1.7 (they must be visually distinct, - not two variants of the same hue at similar lightness) - - accent must carry readable text on a filled badge/pill: EITHER - saturated (chroma ≥ 0.10) OR clearly light (L ≥ 0.85) OR clearly - dark (L ≤ 0.30). Never a muddy mid-tone (L 0.45-0.72 + chroma < 0.10) - — taupe/mushroom/dusty-grey accents read as weak and can't hold text - either way. Saturate it or push its lightness to a clear light/dark. - - avoid the saturated AI attractor zones: claude-beige (warm-cream bg - + dusty brown primary), forest-green-on-cream, AI-purple-on-white, - navy-cream-with-orange-accent + - OKLCH only — never hex. Never #RRGGBB. + - ink-vs-bg WCAG contrast ≥ 7 (body text must be readable) + - primary chroma ≤ 0.23 (above this, primary glows perceptually and + no text on it is readable — acid-bright is a UI failure) + - if primary L > 0.78, primary chroma ≤ 0.18 (the fluorescent zone) + - primary-vs-accent contrast ≥ 1.7 (they must be visually distinct, + not two variants of the same hue at similar lightness) + - accent must carry readable text on a filled badge/pill: EITHER + saturated (chroma ≥ 0.10) OR clearly light (L ≥ 0.85) OR clearly + dark (L ≤ 0.30). Never a muddy mid-tone (L 0.45-0.72 + chroma < 0.10) + — taupe/mushroom/dusty-grey accents read as weak and can't hold text + either way. Saturate it or push its lightness to a clear light/dark. + - avoid the saturated AI attractor zones: claude-beige (warm-cream bg + + dusty brown primary), forest-green-on-cream, AI-purple-on-white, + navy-cream-with-orange-accent - TEXT-ON-COLOR FILLS — pick by perceptual contrast, not just WCAG. The - rule applies to ANY element where text sits on a saturated color fill: - primary buttons, accent buttons, badges, status pills, tag highlights, - filled callouts. Don't only think "primary button" — apply consistently. +TEXT-ON-COLOR FILLS — pick by perceptual contrast, not just WCAG. The +rule applies to ANY element where text sits on a saturated color fill: +primary buttons, accent buttons, badges, status pills, tag highlights, +filled callouts. Don't only think "primary button" — apply consistently. - For any saturated mid-luminance color (L between 0.42 and 0.78, chroma ≥ - 0.08), use WHITE text (or near-white from your bg), not dark text — even - if WCAG says dark technically passes. The Helmholtz-Kohlrausch effect - makes saturated colors appear brighter than their luminance suggests, - and dark text on a warm-or-cool-saturated fill reads as muddy. +For any saturated mid-luminance color (L between 0.42 and 0.78, chroma ≥ +0.08), use WHITE text (or near-white from your bg), not dark text — even +if WCAG says dark technically passes. The Helmholtz-Kohlrausch effect +makes saturated colors appear brighter than their luminance suggests, +and dark text on a warm-or-cool-saturated fill reads as muddy. - Convention: Stripe orange CTAs, McDonald's red, every fintech orange - button, Vercel's filled badges, Linear's status pills — all use white - text on saturated bg fills. +Convention: saturated action fills in the wild, from fast-food reds to +status pills to filled badges, near-universally carry white text. - Dark text is correct only on PALE fills (L > 0.85) or PURE-NEUTRAL fills - (chroma near 0). Everything else: white text. +Dark text is correct only on PALE fills (L > 0.85) or PURE-NEUTRAL fills +(chroma near 0). Everything else: white text. - Return your composed palette in CSS custom properties using OKLCH, then - build with it. The seed is the start, not the recipe. - `); +Return your composed palette in CSS custom properties using OKLCH, then +build with it. The seed is the start, not the recipe. +`); } diff --git a/skill/scripts/picker-server.mjs b/skill/scripts/picker-server.mjs index 6b8f521eb..ef8191011 100644 --- a/skill/scripts/picker-server.mjs +++ b/skill/scripts/picker-server.mjs @@ -10,7 +10,7 @@ import { readFile, mkdir, stat, writeFile } from 'node:fs/promises'; import net from 'node:net'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { extractRegister } from './context.mjs'; +import { extractSectionValue } from './context.mjs'; import { SEEDS } from './palette.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const pickerDir = path.join(scriptDir, 'picker'); @@ -173,7 +173,8 @@ if (options.help) { let register = 'brand'; try { const product = await readFile(path.resolve(process.cwd(), 'PRODUCT.md'), 'utf8'); - register = extractRegister(product) || register; + const value = (extractSectionValue(product, 'Register') || '').toLowerCase(); + if (value === 'brand' || value === 'product') register = value; } catch { // A missing or unreadable PRODUCT.md keeps the brand preview default. }